diff --git a/Sources/MacroToolkit/ClassRestrictionType.swift b/Sources/MacroToolkit/ClassRestrictionType.swift index d90097d..3177256 100644 --- a/Sources/MacroToolkit/ClassRestrictionType.swift +++ b/Sources/MacroToolkit/ClassRestrictionType.swift @@ -5,7 +5,10 @@ public struct ClassRestrictionType: TypeProtocol { public var _baseSyntax: ClassRestrictionTypeSyntax public var _attributedSyntax: AttributedTypeSyntax? - public init(_ syntax: ClassRestrictionTypeSyntax, attributedSyntax: AttributedTypeSyntax? = nil) { + public init( + _ syntax: ClassRestrictionTypeSyntax, + attributedSyntax: AttributedTypeSyntax? = nil + ) { _baseSyntax = syntax _attributedSyntax = attributedSyntax } diff --git a/Sources/MacroToolkit/DeclGroup.swift b/Sources/MacroToolkit/DeclGroup.swift deleted file mode 100644 index 4130e09..0000000 --- a/Sources/MacroToolkit/DeclGroup.swift +++ /dev/null @@ -1,33 +0,0 @@ -import SwiftSyntax - -// TODO: Enable initializing from an `any DeclGroupSyntax`. -/// Wraps a declaration group (a declaration with a scoped block of members). -/// For example an `enum` or a `struct` etc. -public struct DeclGroup: DeclGroupProtocol { - public var _syntax: WrappedSyntax - - public init(_ syntax: WrappedSyntax) { - _syntax = syntax - } - - public var identifier: String { - if let `struct` = asStruct { - `struct`.identifier - } else if let `enum` = asEnum { - `enum`.identifier - } else { - // TODO: Implement wrappers for all other decl group types. - fatalError("Unhandled decl group type '\(type(of: _syntax))'") - } - } - - /// Gets the decl group as a struct if it's a struct. - public var asStruct: Struct? { - Struct(_syntax) - } - - /// Gets the decl group as an enum if it's an enum. - public var asEnum: Enum? { - Enum(_syntax) - } -} diff --git a/Sources/MacroToolkit/DeclGroup/Actor.swift b/Sources/MacroToolkit/DeclGroup/Actor.swift new file mode 100644 index 0000000..c33e03c --- /dev/null +++ b/Sources/MacroToolkit/DeclGroup/Actor.swift @@ -0,0 +1,19 @@ +import SwiftSyntax + +/// Wraps an `actor` declaration. +public struct Actor: DeclGroupProtocol, RepresentableBySyntax { + /// The underlying syntax node for the `actor` declaration. + public var _syntax: ActorDeclSyntax + + /// The identifier (name) of the `actor`. + public var identifier: String { + _syntax.name.withoutTrivia().text + } + + /// Initializes an `Actor` instance with the given syntax node. + /// + /// - Parameter syntax: The syntax node representing the `actor` declaration. + public init(_ syntax: ActorDeclSyntax) { + _syntax = syntax + } +} diff --git a/Sources/MacroToolkit/DeclGroup/Class.swift b/Sources/MacroToolkit/DeclGroup/Class.swift new file mode 100644 index 0000000..0b7f44e --- /dev/null +++ b/Sources/MacroToolkit/DeclGroup/Class.swift @@ -0,0 +1,19 @@ +import SwiftSyntax + +/// Wraps a `class` declaration. +public struct Class: DeclGroupProtocol, RepresentableBySyntax { + /// The underlying syntax node for the `class` declaration. + public var _syntax: ClassDeclSyntax + + /// The identifier (name) of the `class`. + public var identifier: String { + _syntax.name.withoutTrivia().text + } + + /// Initializes a `Class` instance with the given syntax node. + /// + /// - Parameter syntax: The syntax node representing the `class` declaration. + public init(_ syntax: ClassDeclSyntax) { + _syntax = syntax + } +} diff --git a/Sources/MacroToolkit/DeclGroup/DeclGroup.swift b/Sources/MacroToolkit/DeclGroup/DeclGroup.swift new file mode 100644 index 0000000..837d244 --- /dev/null +++ b/Sources/MacroToolkit/DeclGroup/DeclGroup.swift @@ -0,0 +1,57 @@ +import SwiftSyntax + +/// 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 `DeclGroupProtocol`, +/// allowing access to common properties of declaration groups. +public enum DeclGroup: DeclGroupProtocol { + case `struct`(Struct) + case `enum`(Enum) + case `class`(Class) + 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 + case .enum(let wrapped): return wrapped + case .class(let wrapped): return wrapped + case .actor(let wrapped): return wrapped + case .extension(let wrapped): return wrapped + } + } + + /// Initializes a `DeclGroup` instance from a `DeclGroupSyntax`. + /// + /// - 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(_ syntax: DeclGroupSyntax) { + if let syntax = syntax.as(ActorDeclSyntax.self) { + self = .actor(Actor(syntax)) + } else if let syntax = syntax.as(ClassDeclSyntax.self) { + self = .class(Class(syntax)) + } else if let syntax = syntax.as(EnumDeclSyntax.self) { + self = .enum(Enum(syntax)) + } else if let syntax = syntax.as(ExtensionDeclSyntax.self) { + self = .extension(Extension(syntax)) + } else if let syntax = syntax.as(StructDeclSyntax.self) { + self = .struct(Struct(syntax)) + } else { + fatalError("Unhandled decl group type '\(type(of: syntax))'") + } + } + + /// The identifier of the declaration group. + public var identifier: String { wrapped.identifier } + + /// All members declared within the declaration group. + public var members: [Decl] { wrapped.members } + + /// All properties declared within the declaration group. + public var properties: [Property] { wrapped.properties } + + /// All types that the declaration group inherits from or conforms to. + public var inheritedTypes: [Type] { wrapped.inheritedTypes } +} diff --git a/Sources/MacroToolkit/DeclGroup/DeclGroupProtocol.swift b/Sources/MacroToolkit/DeclGroup/DeclGroupProtocol.swift new file mode 100644 index 0000000..10a1495 --- /dev/null +++ b/Sources/MacroToolkit/DeclGroup/DeclGroupProtocol.swift @@ -0,0 +1,64 @@ +import SwiftSyntax + +/// 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. +public protocol DeclGroupProtocol { + /// The identifier of the declaration group. + var identifier: String { get } + + /// All members declared within the declaration group. + var members: [Decl] { get } + + /// All properties declared within the declaration group. + var properties: [Property] { get } + + /// All types that the declaration group inherits from or conforms to. + var inheritedTypes: [Type] { get } +} + +extension DeclGroupProtocol where UnderlyingSyntax: DeclGroupSyntax, Self: RepresentableBySyntax { + /// 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 syntax = syntax as? UnderlyingSyntax else { return nil } + self.init(syntax) + } + + public var members: [Decl] { + _syntax.memberBlock.members.map(\.decl).map(Decl.init) + } + + public var properties: [Property] { + members.compactMap(\.asVariable).flatMap { variable in + var bindings = variable._syntax.bindings.flatMap { binding in + Property.properties(from: binding, in: variable) + } + // For the declaration `var a, b: Int` where `a` doesn't have an annotation, + // `a` gets given the type of `b` (`Int`). To implement this, we 'drag' the + // type annotations backwards over the non-annotated bindings. + var lastSeenType: Type? + for (i, binding) in bindings.enumerated().reversed() { + if let type = binding.type { + lastSeenType = type + } else { + bindings[i].type = lastSeenType + } + } + return bindings + } + } + + public var inheritedTypes: [Type] { + _syntax.inheritanceClause?.inheritedTypes.map(\.type).map(Type.init) ?? [] + } + + public var accessLevel: AccessModifier? { + AccessModifier(firstModifierOfKindIn: _syntax.modifiers) + } + + public var declarationContext: DeclarationContextModifier? { + DeclarationContextModifier(firstModifierOfKindIn: _syntax.modifiers) + } +} diff --git a/Sources/MacroToolkit/Enum.swift b/Sources/MacroToolkit/DeclGroup/Enum.swift similarity index 63% rename from Sources/MacroToolkit/Enum.swift rename to Sources/MacroToolkit/DeclGroup/Enum.swift index c866994..c1c7e3f 100644 --- a/Sources/MacroToolkit/Enum.swift +++ b/Sources/MacroToolkit/DeclGroup/Enum.swift @@ -1,13 +1,18 @@ import SwiftSyntax /// Wraps an `enum` declaration. -public struct Enum: DeclGroupProtocol { +public struct Enum: DeclGroupProtocol, RepresentableBySyntax { + /// The underlying syntax node for the `enum` declaration. public var _syntax: EnumDeclSyntax + /// The identifier (name) of the `enum`. public var identifier: String { _syntax.name.withoutTrivia().text } + /// Initializes an `Enum` instance with the given syntax node. + /// + /// - Parameter syntax: The syntax node representing the `enum` declaration. public init(_ syntax: EnumDeclSyntax) { _syntax = syntax } diff --git a/Sources/MacroToolkit/DeclGroup/Extension.swift b/Sources/MacroToolkit/DeclGroup/Extension.swift new file mode 100644 index 0000000..d415d40 --- /dev/null +++ b/Sources/MacroToolkit/DeclGroup/Extension.swift @@ -0,0 +1,19 @@ +import SwiftSyntax + +/// Wraps an `extension` declaration. +public struct Extension: DeclGroupProtocol, RepresentableBySyntax { + /// The underlying syntax node for the `extension` declaration. + public var _syntax: ExtensionDeclSyntax + + /// The identifier (extended type) of the `extension`. + public var identifier: String { + _syntax.extendedType.withoutTrivia().description + } + + /// Initializes an `Extension` instance with the given syntax node. + /// + /// - Parameter syntax: The syntax node representing the `extension` declaration. + public init(_ syntax: ExtensionDeclSyntax) { + _syntax = syntax + } +} diff --git a/Sources/MacroToolkit/DeclGroup/Struct.swift b/Sources/MacroToolkit/DeclGroup/Struct.swift new file mode 100644 index 0000000..5eafdfb --- /dev/null +++ b/Sources/MacroToolkit/DeclGroup/Struct.swift @@ -0,0 +1,19 @@ +import SwiftSyntax + +/// Wraps a `struct` declaration. +public struct Struct: DeclGroupProtocol, RepresentableBySyntax { + /// The underlying syntax node for the `struct` declaration. + public var _syntax: StructDeclSyntax + + /// The identifier (name) of the `struct`. + public var identifier: String { + _syntax.name.withoutTrivia().text + } + + /// Initializes a `Struct` instance with the given syntax node. + /// + /// - Parameter syntax: The syntax node representing the `struct` declaration. + public init(_ syntax: StructDeclSyntax) { + _syntax = syntax + } +} diff --git a/Sources/MacroToolkit/DeclGroupProtocol.swift b/Sources/MacroToolkit/DeclGroupProtocol.swift deleted file mode 100644 index a4cc36e..0000000 --- a/Sources/MacroToolkit/DeclGroupProtocol.swift +++ /dev/null @@ -1,71 +0,0 @@ -import SwiftSyntax - -/// A declaration group (e.g. a `struct` or `class` rather than a regular -/// declaration such as `var`). -public protocol DeclGroupProtocol { - /// The type of the underlying syntax node being wrapped. - associatedtype WrappedSyntax: DeclGroupSyntax - /// The underlying syntax node. - var _syntax: WrappedSyntax { get } - /// The declaration's identifier. - /// - /// For some reason SwiftSyntax's `DeclGroupSyntax` protocol doesn't have the - /// declaration's identifier, so this needs to be implemented manually - /// for every declaration wrapper. Maybe due to extensions technically not - /// having a name? (although they're always attached to a specific identifier). - var identifier: String { get } - /// Wraps a syntax node. - init(_ syntax: WrappedSyntax) - -} - -extension DeclGroupProtocol { - /// Attempts to initialize the wrapper from an arbitrary decl group (succeeds - /// if the decl group is the right type of syntax). - public init?(_ syntax: any DeclGroupSyntax) { - guard let syntax = syntax.as(WrappedSyntax.self) else { - return nil - } - self.init(syntax) - } - - /// The declaration group's members. - public var members: [Decl] { - _syntax.memberBlock.members.map(\.decl).map(Decl.init) - } - - /// The declaration group's declared properties. - public var properties: [Property] { - members.compactMap(\.asVariable).flatMap { variable in - var bindings = variable._syntax.bindings.flatMap { binding in - Property.properties(from: binding, in: variable) - } - // For the declaration `var a, b: Int` where `a` doesn't have an annotation, - // `a` gets given the type of `b` (`Int`). To implement this, we 'drag' the - // type annotations backwards over the non-annotated bindings. - var lastSeenType: Type? - for (i, binding) in bindings.enumerated().reversed() { - if let type = binding.type { - lastSeenType = type - } else { - bindings[i].type = lastSeenType - } - } - return bindings - } - } - - /// The types inherited from or conformed to by the decl group. Doesn't - /// include conformances added by other declaration groups such as an - /// `extension` of the current declaration. - public var inheritedTypes: [Type] { - _syntax.inheritanceClause?.inheritedTypes.map(\.type).map(Type.init) ?? [] - } - - // TODO: Replace this with an accessLevel property - /// Whether the declaration was declared with the `public` access level - /// modifier. - public var isPublic: Bool { - _syntax.isPublic - } -} diff --git a/Sources/MacroToolkit/FunctionParameter.swift b/Sources/MacroToolkit/FunctionParameter.swift index 9199955..da08c4d 100644 --- a/Sources/MacroToolkit/FunctionParameter.swift +++ b/Sources/MacroToolkit/FunctionParameter.swift @@ -60,7 +60,8 @@ extension Sequence where Element == FunctionParameter { let parameters = Array(self) for (index, parameter) in parameters.enumerated() { let isLast = index == parameters.count - 1 - let syntax = parameter._syntax.with(\.trailingComma, isLast ? nil : TokenSyntax.commaToken()) + let syntax = parameter._syntax + .with(\.trailingComma, isLast ? nil : TokenSyntax.commaToken()) list += [syntax] } return list diff --git a/Sources/MacroToolkit/ImplicitlyUnwrappedOptionalType.swift b/Sources/MacroToolkit/ImplicitlyUnwrappedOptionalType.swift index 9d51c1c..9f3e1b3 100644 --- a/Sources/MacroToolkit/ImplicitlyUnwrappedOptionalType.swift +++ b/Sources/MacroToolkit/ImplicitlyUnwrappedOptionalType.swift @@ -5,7 +5,10 @@ public struct ImplicitlyUnwrappedOptionalType: TypeProtocol { public var _baseSyntax: ImplicitlyUnwrappedOptionalTypeSyntax public var _attributedSyntax: AttributedTypeSyntax? - public init(_ syntax: ImplicitlyUnwrappedOptionalTypeSyntax, attributedSyntax: AttributedTypeSyntax? = nil) { + public init( + _ syntax: ImplicitlyUnwrappedOptionalTypeSyntax, + attributedSyntax: AttributedTypeSyntax? = nil + ) { _baseSyntax = syntax _attributedSyntax = attributedSyntax } diff --git a/Sources/MacroToolkit/Modifiers/AccessLevel.swift b/Sources/MacroToolkit/Modifiers/AccessLevel.swift new file mode 100644 index 0000000..09b0a7e --- /dev/null +++ b/Sources/MacroToolkit/Modifiers/AccessLevel.swift @@ -0,0 +1,69 @@ +import SwiftSyntax + +/// Represents access control levels in Swift (e.g., private, public). +public enum AccessModifier: RawRepresentable, ModifierProtocol, Comparable { + case `private` + case `fileprivate` + case `internal` + case `package` + case `public` + case `open` + + /// Initializes an `AccessModifier` from a `TokenKind`. + /// + /// - Parameter rawValue: The `TokenKind` representing an access control keyword. + public init?(rawValue: TokenKind) { + switch rawValue { + case .keyword(.private): + self = .private + case .keyword(.fileprivate): + self = .fileprivate + case .keyword(.internal): + self = .internal + case .keyword(.package): + self = .package + case .keyword(.public): + self = .public + case .keyword(.open): + self = .open + default: + return nil + } + } + + /// The `TokenKind` corresponding to the `AccessModifier`. + public var rawValue: TokenKind { + switch self { + case .private: + return .keyword(.private) + case .fileprivate: + return .keyword(.fileprivate) + case .internal: + return .keyword(.internal) + case .package: + return .keyword(.package) + case .public: + return .keyword(.public) + case .open: + return .keyword(.open) + } + } + + /// The string name of the `AccessModifier`. + public var name: String { + switch self { + case .private: + return "private" + case .fileprivate: + return "fileprivate" + case .internal: + return "internal" + case .package: + return "package" + case .public: + return "public" + case .open: + return "open" + } + } +} diff --git a/Sources/MacroToolkit/Modifiers/DeclarationContextModifier.swift b/Sources/MacroToolkit/Modifiers/DeclarationContextModifier.swift new file mode 100644 index 0000000..4bcb81f --- /dev/null +++ b/Sources/MacroToolkit/Modifiers/DeclarationContextModifier.swift @@ -0,0 +1,31 @@ +import SwiftSyntax + +/// Represents context-specific modifiers for declarations (e.g., static, class). +public enum DeclarationContextModifier: RawRepresentable, ModifierProtocol { + case `static` + case `class` + + /// Initializes a `DeclarationContextModifier` from a `TokenKind`. + /// + /// - Parameter rawValue: The `TokenKind` representing a context-specific keyword. + public init?(rawValue: TokenKind) { + switch rawValue { + case .keyword(.static): + self = .static + case .keyword(.class): + self = .class + default: + return nil + } + } + + /// The `TokenKind` corresponding to the `DeclarationContextModifier`. + public var rawValue: TokenKind { + switch self { + case .static: + return .keyword(.static) + case .class: + return .keyword(.class) + } + } +} diff --git a/Sources/MacroToolkit/Modifiers/ModifierProtocol.swift b/Sources/MacroToolkit/Modifiers/ModifierProtocol.swift new file mode 100644 index 0000000..47b7608 --- /dev/null +++ b/Sources/MacroToolkit/Modifiers/ModifierProtocol.swift @@ -0,0 +1,23 @@ +import SwiftSyntax + +/// A protocol for modifiers in Swift that are represented by `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?(firstModifierOfKindIn: DeclModifierListSyntax) +} + +extension ModifierProtocol { + /// Default implementation for initializing a `Modifier` from a list of declaration modifiers. + /// + /// - Parameter modifiers: A list of declaration modifiers. + public init?(firstModifierOfKindIn: DeclModifierListSyntax) { + for element in firstModifierOfKindIn { + guard let modifier = Self(rawValue: element.name.tokenKind) else { continue } + self = modifier + return + } + return nil + } +} diff --git a/Sources/MacroToolkit/Property.swift b/Sources/MacroToolkit/Property.swift index 3aa2e30..da9ec23 100644 --- a/Sources/MacroToolkit/Property.swift +++ b/Sources/MacroToolkit/Property.swift @@ -32,19 +32,21 @@ public struct Property { static func properties(from binding: PatternBindingSyntax, in decl: Variable) -> [Property] { - let accessors: [AccessorDeclSyntax] = switch binding.accessorBlock?.accessors { - case .accessors(let block): - Array(block) - case .getter(let getter): - [AccessorDeclSyntax(accessorSpecifier: .keyword(.get)) { getter }] - case .none: + let accessors: [AccessorDeclSyntax] = + switch binding.accessorBlock?.accessors { + case .accessors(let block): + Array(block) + case .getter(let getter): + [AccessorDeclSyntax(accessorSpecifier: .keyword(.get)) { getter }] + case .none: + [] + } + let attributes: [AttributeListElement] = + if decl.bindings.count == 1 { + decl.attributes + } else { [] - } - let attributes: [AttributeListElement] = if decl.bindings.count == 1 { - decl.attributes - } else { - [] - } + } return properties( pattern: binding.pattern, initialValue: (binding.initializer?.value).map(Expr.init), @@ -67,25 +69,26 @@ public struct Property { ) -> [Property] { switch pattern.asProtocol(PatternSyntaxProtocol.self) { case let pattern as IdentifierPatternSyntax: - let type: Type? = if let type { - type - } else { - if initialValue?.asIntegerLiteral != nil { - Type("Int") - } else if initialValue?.asFloatLiteral != nil { - Type("Double") - } else if initialValue?.asStringLiteral != nil { - Type("String") - } else if initialValue?.asBooleanLiteral != nil { - Type("Bool") - } else if initialValue?.asRegexLiteral != nil { - Type("Regex") - } else if let array = initialValue?._syntax.as(ArrayExprSyntax.self) { - inferArrayLiteralType(array) + let type: Type? = + if let type { + type } else { - nil + if initialValue?.asIntegerLiteral != nil { + Type("Int") + } else if initialValue?.asFloatLiteral != nil { + Type("Double") + } else if initialValue?.asStringLiteral != nil { + Type("String") + } else if initialValue?.asBooleanLiteral != nil { + Type("Bool") + } else if initialValue?.asRegexLiteral != nil { + Type("Regex") + } else if let array = initialValue?._syntax.as(ArrayExprSyntax.self) { + inferArrayLiteralType(array) + } else { + nil + } } - } return [ Property( _syntax: pattern.identifier, @@ -101,46 +104,47 @@ public struct Property { case let pattern as TuplePatternSyntax: let tupleInitialValue: TupleExprSyntax? = if let initialValue, let tuple = initialValue._syntax.as(TupleExprSyntax.self), - tuple.elements.count == pattern.elements.count - { - tuple - } else { - nil - } + tuple.elements.count == pattern.elements.count + { + tuple + } else { + nil + } let tupleType: TupleType? = if let type, - let tuple = TupleType(type), - tuple.elements.count == pattern.elements.count - { - tuple - } else { - nil - } - return pattern.elements.enumerated().flatMap { (index, element) in - let initialValue = if let tupleInitialValue { - Expr(Array(tupleInitialValue.elements)[index].expression) + let tuple = TupleType(type), + tuple.elements.count == pattern.elements.count + { + tuple } else { - initialValue.map { expr in - Expr( - MemberAccessExprSyntax( - leadingTrivia: nil, base: expr._syntax.parenthesized, - period: .periodToken(), - name: .identifier(String(index)), trailingTrivia: nil + nil + } + return pattern.elements.enumerated().flatMap { (index, element) in + let initialValue = + if let tupleInitialValue { + Expr(Array(tupleInitialValue.elements)[index].expression) + } else { + initialValue.map { expr in + Expr( + MemberAccessExprSyntax( + leadingTrivia: nil, base: expr._syntax.parenthesized, + period: .periodToken(), + name: .identifier(String(index)), trailingTrivia: nil + ) ) - ) + } } - } // If in a tuple initial value expression, an empty array literal is inferred to have // type `Array`, unlike with regular initial value expressions. let type = if let arrayLiteral = initialValue?._syntax.as(ArrayExprSyntax.self), - arrayLiteral.elements.isEmpty - { - Type("Array") - } else { - tupleType?.elements[index] - } + arrayLiteral.elements.isEmpty + { + Type("Array") + } else { + tupleType?.elements[index] + } // Tuple bindings can't have accessors or attributes (i.e. property wrappers or macros) return properties( diff --git a/Sources/MacroToolkit/RepresentableBySyntax.swift b/Sources/MacroToolkit/RepresentableBySyntax.swift new file mode 100644 index 0000000..176aaff --- /dev/null +++ b/Sources/MacroToolkit/RepresentableBySyntax.swift @@ -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 } + + /// Initializes an instance with the given underlying syntax node. + /// + /// - Parameter syntax: The underlying syntax node to represent. + init(_ syntax: UnderlyingSyntax) +} diff --git a/Sources/MacroToolkit/Struct.swift b/Sources/MacroToolkit/Struct.swift deleted file mode 100644 index b154e9d..0000000 --- a/Sources/MacroToolkit/Struct.swift +++ /dev/null @@ -1,14 +0,0 @@ -import SwiftSyntax - -/// Wraps a `struct` declaration. -public struct Struct: DeclGroupProtocol { - public var _syntax: StructDeclSyntax - - public var identifier: String { - _syntax.name.withoutTrivia().text - } - - public init(_ syntax: StructDeclSyntax) { - _syntax = syntax - } -} diff --git a/Sources/MacroToolkit/Type.swift b/Sources/MacroToolkit/Type.swift index d4bd394..746205b 100644 --- a/Sources/MacroToolkit/Type.swift +++ b/Sources/MacroToolkit/Type.swift @@ -39,46 +39,48 @@ public enum `Type`: TypeProtocol, SyntaxExpressibleByStringInterpolation { case tuple(TupleType) public var _baseSyntax: TypeSyntax { - let type: any TypeProtocol = switch self { - case .array(let type): type - case .classRestriction(let type): type - case .composition(let type): type - case .someOrAny(let type): type - case .dictionary(let type): type - case .function(let type): type - case .implicitlyUnwrappedOptional(let type): type - case .member(let type): type - case .metatype(let type): type - case .missing(let type): type - case .optional(let type): type - case .packExpansion(let type): type - case .packReference(let type): type - case .simple(let type): type - case .suppressed(let type): type - case .tuple(let type): type - } + let type: any TypeProtocol = + switch self { + case .array(let type): type + case .classRestriction(let type): type + case .composition(let type): type + case .someOrAny(let type): type + case .dictionary(let type): type + case .function(let type): type + case .implicitlyUnwrappedOptional(let type): type + case .member(let type): type + case .metatype(let type): type + case .missing(let type): type + case .optional(let type): type + case .packExpansion(let type): type + case .packReference(let type): type + case .simple(let type): type + case .suppressed(let type): type + case .tuple(let type): type + } return TypeSyntax(type._baseSyntax) } public var _attributedSyntax: AttributedTypeSyntax? { - let type: any TypeProtocol = switch self { - case .array(let type): type - case .classRestriction(let type): type - case .composition(let type): type - case .someOrAny(let type): type - case .dictionary(let type): type - case .function(let type): type - case .implicitlyUnwrappedOptional(let type): type - case .member(let type): type - case .metatype(let type): type - case .missing(let type): type - case .optional(let type): type - case .packExpansion(let type): type - case .packReference(let type): type - case .simple(let type): type - case .suppressed(let type): type - case .tuple(let type): type - } + let type: any TypeProtocol = + switch self { + case .array(let type): type + case .classRestriction(let type): type + case .composition(let type): type + case .someOrAny(let type): type + case .dictionary(let type): type + case .function(let type): type + case .implicitlyUnwrappedOptional(let type): type + case .member(let type): type + case .metatype(let type): type + case .missing(let type): type + case .optional(let type): type + case .packExpansion(let type): type + case .packReference(let type): type + case .simple(let type): type + case .suppressed(let type): type + case .tuple(let type): type + } return type._attributedSyntax } diff --git a/Sources/MacroToolkitExamplePlugin/AddAsyncAllMembersMacro.swift b/Sources/MacroToolkitExamplePlugin/AddAsyncAllMembersMacro.swift index 15cd34a..9bf7182 100644 --- a/Sources/MacroToolkitExamplePlugin/AddAsyncAllMembersMacro.swift +++ b/Sources/MacroToolkitExamplePlugin/AddAsyncAllMembersMacro.swift @@ -1,9 +1,12 @@ -import SwiftSyntax import MacroToolkit +import SwiftSyntax import SwiftSyntaxMacros public enum AddAsyncAllMembersMacro: MemberMacro { - public static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { + public static func expansion( + of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { declaration.memberBlock.members.map(\.decl).compactMap { try? AddAsyncMacroCore.expansion(of: nil, providingFunctionOf: $0) } diff --git a/Sources/MacroToolkitExamplePlugin/AddAsyncMacroCore.swift b/Sources/MacroToolkitExamplePlugin/AddAsyncMacroCore.swift index 25e64eb..496717d 100644 --- a/Sources/MacroToolkitExamplePlugin/AddAsyncMacroCore.swift +++ b/Sources/MacroToolkitExamplePlugin/AddAsyncMacroCore.swift @@ -1,11 +1,14 @@ -import SwiftSyntax import MacroToolkit +import SwiftSyntax import SwiftSyntaxMacros // Modified from: https://github.com/DougGregor/swift-macro-examples/blob/f61ac7cdca8dc3557e53f86e7e03df1353908d3e/MacroExamplesPlugin/AddAsyncMacro.swift enum AddAsyncMacroCore { - static func expansion(of node: AttributeSyntax?, providingFunctionOf declaration: some DeclSyntaxProtocol) throws -> DeclSyntax { + static func expansion( + of node: AttributeSyntax?, + providingFunctionOf declaration: some DeclSyntaxProtocol + ) throws -> DeclSyntax { // Only on functions at the moment. guard let function = Function(declaration) else { throw MacroError("@AddAsync only works on functions") @@ -65,28 +68,28 @@ enum AddAsyncMacroCore { let newBody = function._syntax.body.map { _ in let switchBody: ExprSyntax = - """ - switch returnValue { - case .success(let value): - continuation.resume(returning: value) - case .failure(let error): - continuation.resume(throwing: error) - } - """ - + """ + switch returnValue { + case .success(let value): + continuation.resume(returning: value) + case .failure(let error): + continuation.resume(throwing: error) + } + """ + let continuationExpr = - isResultReturn - ? "try await withCheckedThrowingContinuation { continuation in" - : "await withCheckedContinuation { continuation in" - + isResultReturn + ? "try await withCheckedThrowingContinuation { continuation in" + : "await withCheckedContinuation { continuation in" + let newBody: ExprSyntax = - """ - \(raw: continuationExpr) - \(raw: function.identifier)(\(raw: callArguments.joined(separator: ", "))) { returnValue in - \(isResultReturn ? switchBody : "continuation.resume(returning: returnValue)") + """ + \(raw: continuationExpr) + \(raw: function.identifier)(\(raw: callArguments.joined(separator: ", "))) { returnValue in + \(isResultReturn ? switchBody : "continuation.resume(returning: returnValue)") + } } - } - """ + """ return CodeBlockSyntax([newBody]) } // TODO: Make better codeblock init diff --git a/Sources/MacroToolkitExamplePlugin/CaseDetectionMacro.swift b/Sources/MacroToolkitExamplePlugin/CaseDetectionMacro.swift index 80c1892..24670f4 100644 --- a/Sources/MacroToolkitExamplePlugin/CaseDetectionMacro.swift +++ b/Sources/MacroToolkitExamplePlugin/CaseDetectionMacro.swift @@ -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 { @@ -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) } @@ -29,4 +29,4 @@ public struct CaseDetectionMacro: MemberMacro { """ } } -} \ No newline at end of file +} diff --git a/Sources/MacroToolkitExamplePlugin/DictionaryStorageMacro.swift b/Sources/MacroToolkitExamplePlugin/DictionaryStorageMacro.swift index 0bb677e..c9960b8 100644 --- a/Sources/MacroToolkitExamplePlugin/DictionaryStorageMacro.swift +++ b/Sources/MacroToolkitExamplePlugin/DictionaryStorageMacro.swift @@ -1,6 +1,6 @@ +import MacroToolkit import SwiftSyntax import SwiftSyntaxMacros -import MacroToolkit public struct DictionaryStorageMacro {} diff --git a/Sources/MacroToolkitExamplePlugin/MetaEnumMacro.swift b/Sources/MacroToolkitExamplePlugin/MetaEnumMacro.swift index ba2fa97..9a0f2c1 100644 --- a/Sources/MacroToolkitExamplePlugin/MetaEnumMacro.swift +++ b/Sources/MacroToolkitExamplePlugin/MetaEnumMacro.swift @@ -22,7 +22,7 @@ public struct MetaEnumMacro { parentTypeName = enumDecl.identifier - access = enumDecl.isPublic ? "public " : "" + access = enumDecl.accessLevel == .public ? "public " : "" metaCases = enumDecl.cases.map { case_ in case_.withoutValue() @@ -90,7 +90,11 @@ enum CaseMacroDiagnostic { func diagnose(at node: Syntax) -> Diagnostic { DiagnosticBuilder(for: node) .message(message) - .messageID(MessageID(domain: "MetaEnum", id: Mirror(reflecting: self).children.first?.label ?? "\(self)")) + .messageID( + MessageID( + domain: "MetaEnum", + id: Mirror(reflecting: self).children.first?.label ?? "\(self)") + ) .build() } } diff --git a/Sources/MacroToolkitExamplePlugin/OptionSetMacro.swift b/Sources/MacroToolkitExamplePlugin/OptionSetMacro.swift index 00d2361..835b586 100644 --- a/Sources/MacroToolkitExamplePlugin/OptionSetMacro.swift +++ b/Sources/MacroToolkitExamplePlugin/OptionSetMacro.swift @@ -149,8 +149,7 @@ extension OptionSetMacro: MemberMacro { let cases = optionsEnum.cases - // TODO: This seems wrong, surely other modifiers would also make sense to passthrough? - let access = structDecl.isPublic ? "public " : "" + let access = structDecl.accessLevel.map { "\($0) " } ?? "" let staticVars = cases.map { (case_) -> DeclSyntax in """ diff --git a/Tests/MacroToolkitTests/DeclGroupTests.swift b/Tests/MacroToolkitTests/DeclGroupTests.swift new file mode 100644 index 0000000..6d22fa2 --- /dev/null +++ b/Tests/MacroToolkitTests/DeclGroupTests.swift @@ -0,0 +1,163 @@ +import SwiftSyntax +import XCTest + +@testable import MacroToolkit + +final class DeclGroupTests: XCTestCase { + func testStructInitialization() throws { + let decl: DeclSyntax = """ + struct TestStruct { var value: Int } + """ + let structDecl = decl.as(StructDeclSyntax.self)! + let testStruct = Struct(structDecl) + + XCTAssertEqual(testStruct.identifier, "TestStruct") + XCTAssertEqual(testStruct.members.count, 1) + XCTAssertEqual(testStruct.properties.count, 1) + } + + func testEnumInitialization() throws { + let decl: DeclSyntax = """ + enum TestEnum { case caseOne, caseTwo } + """ + let enumDecl = decl.as(EnumDeclSyntax.self)! + let testEnum = Enum(enumDecl) + + XCTAssertEqual(testEnum.identifier, "TestEnum") + XCTAssertEqual(testEnum.members.count, 1) + XCTAssertEqual(testEnum.cases.count, 2) + } + + func testClassInitialization() throws { + let decl: DeclSyntax = """ + class TestClass { var value: Int } + """ + let classDecl = decl.as(ClassDeclSyntax.self)! + let testClass = Class(classDecl) + + XCTAssertEqual(testClass.identifier, "TestClass") + XCTAssertEqual(testClass.members.count, 1) + XCTAssertEqual(testClass.properties.count, 1) + } + + func testActorInitialization() throws { + let decl: DeclSyntax = """ + actor TestActor { var value: Int } + """ + let actorDecl = decl.as(ActorDeclSyntax.self)! + let testActor = Actor(actorDecl) + + XCTAssertEqual(testActor.identifier, "TestActor") + XCTAssertEqual(testActor.members.count, 1) + XCTAssertEqual(testActor.properties.count, 1) + } + + func testExtensionInitialization() throws { + let decl: DeclSyntax = """ + extension TestStruct { func testMethod() {} } + """ + let extensionDecl = decl.as(ExtensionDeclSyntax.self)! + let testExtension = Extension(extensionDecl) + + XCTAssertEqual(testExtension.identifier, "TestStruct") + XCTAssertEqual(testExtension.members.count, 1) + } + + func testDeclGroupInitialization() throws { + let structDecl: DeclSyntax = """ + struct TestStruct { var value: Int } + """ + let structSyntax = structDecl.as(StructDeclSyntax.self)! + let structDeclGroup = DeclGroup(structSyntax) + + switch structDeclGroup { + case .struct(let testStruct): + XCTAssertEqual(testStruct.identifier, "TestStruct") + XCTAssertEqual(testStruct.members.count, 1) + XCTAssertEqual(testStruct.properties.count, 1) + default: + XCTFail("Expected .struct case") + } + + let enumDecl: DeclSyntax = """ + enum TestEnum { case caseOne, caseTwo } + """ + let enumSyntax = enumDecl.as(EnumDeclSyntax.self)! + let enumDeclGroup = DeclGroup(enumSyntax) + + switch enumDeclGroup { + case .enum(let testEnum): + XCTAssertEqual(testEnum.identifier, "TestEnum") + XCTAssertEqual(testEnum.members.count, 1) + XCTAssertEqual(testEnum.cases.count, 2) + default: + XCTFail("Expected .enum case") + } + + let classDecl: DeclSyntax = """ + class TestClass { var value: Int } + """ + let classSyntax = classDecl.as(ClassDeclSyntax.self)! + let classDeclGroup = DeclGroup(classSyntax) + + switch classDeclGroup { + case .class(let testClass): + XCTAssertEqual(testClass.identifier, "TestClass") + XCTAssertEqual(testClass.members.count, 1) + XCTAssertEqual(testClass.properties.count, 1) + default: + XCTFail("Expected .class case") + } + + let actorDecl: DeclSyntax = """ + actor TestActor { var value: Int } + """ + let actorSyntax = actorDecl.as(ActorDeclSyntax.self)! + let actorDeclGroup = DeclGroup(actorSyntax) + + switch actorDeclGroup { + case .actor(let testActor): + XCTAssertEqual(testActor.identifier, "TestActor") + XCTAssertEqual(testActor.members.count, 1) + XCTAssertEqual(testActor.properties.count, 1) + default: + XCTFail("Expected .actor case") + } + + let extensionDecl: DeclSyntax = """ + extension TestStruct { func testMethod() {} } + """ + let extensionSyntax = extensionDecl.as(ExtensionDeclSyntax.self)! + let extensionDeclGroup = DeclGroup(extensionSyntax) + + switch extensionDeclGroup { + case .extension(let testExtension): + XCTAssertEqual(testExtension.identifier, "TestStruct") + XCTAssertEqual(testExtension.members.count, 1) + default: + XCTFail("Expected .extension case") + } + } + + func testDeclGroupProtocolExtension() throws { + let decl: DeclSyntax = """ + public class TestClass: SuperClass, ProtocolOne, ProtocolTwo { + public var a: Int + var b: Int + public static var c: Int + func method() {} + } + """ + let classDecl = decl.as(ClassDeclSyntax.self)! + let testClass = Class(classDecl) + + XCTAssertEqual(testClass.identifier, "TestClass") + XCTAssertEqual(testClass.members.count, 4) + XCTAssertEqual(testClass.properties.count, 3) + XCTAssertEqual( + testClass.inheritedTypes.map { $0.description }, + ["SuperClass", "ProtocolOne", "ProtocolTwo"]) + XCTAssertEqual(testClass.accessLevel, .public) + XCTAssertEqual(testClass.declarationContext, nil) + } +} diff --git a/Tests/MacroToolkitTests/ModifierTests.swift b/Tests/MacroToolkitTests/ModifierTests.swift new file mode 100644 index 0000000..67a273c --- /dev/null +++ b/Tests/MacroToolkitTests/ModifierTests.swift @@ -0,0 +1,43 @@ +import SwiftSyntax +import SwiftSyntaxBuilder +import XCTest + +@testable import MacroToolkit + +final class ModifierTests: XCTestCase { + func testAccessModifierInit() { + XCTAssertEqual(AccessModifier(rawValue: .keyword(.private)), .private) + XCTAssertEqual(AccessModifier(rawValue: .keyword(.public)), .public) + XCTAssertNil(AccessModifier(rawValue: .identifier("custom"))) + } + + func testAccessModifierRawValue() { + XCTAssertEqual(AccessModifier.private.rawValue, .keyword(.private)) + XCTAssertEqual(AccessModifier.public.rawValue, .keyword(.public)) + } + + func testAccessModifierName() { + XCTAssertEqual(AccessModifier.private.name, "private") + XCTAssertEqual(AccessModifier.open.name, "open") + } + + func testAccessModifierInitWithModifiers() throws { + let decl: DeclSyntax = """ + private struct Test { } + """ + let structDecl = decl.as(StructDeclSyntax.self) + let structObj = Struct(structDecl!) + XCTAssertEqual(structObj.accessLevel, .private) + } + + func testDeclarationContextModifierInit() { + XCTAssertEqual(DeclarationContextModifier(rawValue: .keyword(.static)), .static) + XCTAssertEqual(DeclarationContextModifier(rawValue: .keyword(.class)), .class) + XCTAssertNil(DeclarationContextModifier(rawValue: .identifier("custom"))) + } + + func testDeclarationContextModifierRawValue() { + XCTAssertEqual(DeclarationContextModifier.static.rawValue, .keyword(.static)) + XCTAssertEqual(DeclarationContextModifier.class.rawValue, .keyword(.class)) + } +}