From ea667a4118d9abf90a30c4e1605eb70807fe59f9 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 18 Jun 2025 13:35:50 -0400 Subject: [PATCH 1/4] [skip ci] --- Examples/Remaining/attributes/code.swift | 14 ++++++++++++++ Examples/Remaining/attributes/dsl.swift | 9 +++++++++ 2 files changed, 23 insertions(+) create mode 100644 Examples/Remaining/attributes/code.swift create mode 100644 Examples/Remaining/attributes/dsl.swift diff --git a/Examples/Remaining/attributes/code.swift b/Examples/Remaining/attributes/code.swift new file mode 100644 index 0000000..5e5e708 --- /dev/null +++ b/Examples/Remaining/attributes/code.swift @@ -0,0 +1,14 @@ +@objc +class Foo { + @Published var bar: String = "bar" + + @available(iOS 17.0, *) + func bar() { + print("bar") + } + + @MainActor + func baz() { + print("baz") + } +} \ No newline at end of file diff --git a/Examples/Remaining/attributes/dsl.swift b/Examples/Remaining/attributes/dsl.swift new file mode 100644 index 0000000..2435ee8 --- /dev/null +++ b/Examples/Remaining/attributes/dsl.swift @@ -0,0 +1,9 @@ +Class("Foo") { + Variable(.var, name: "bar", type: "String", defaultValue: "bar").attribute("Published") + Function("bar") { + print("bar") + }.attribute("available", arguments: ["iOS 17.0", "*"]) + Function("baz") { + print("baz") + }.attribute("MainActor") +}.attribute("objc") \ No newline at end of file From 2da069a04c61139e747fc3c79c510d75623b0181 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 18 Jun 2025 15:09:53 -0400 Subject: [PATCH 2/4] adding attributes --- Examples/Remaining/attributes/dsl.swift | 4 +- Examples/Remaining/attributes/syntax.json | 0 Sources/SyntaxKit/Attribute.swift | 100 +++++++++++++ Sources/SyntaxKit/Class.swift | 88 ++++++++++-- Sources/SyntaxKit/Enum.swift | 58 ++++++++ Sources/SyntaxKit/Extension.swift | 66 ++++++++- Sources/SyntaxKit/Function.swift | 61 +++++++- Sources/SyntaxKit/Parameter.swift | 13 ++ Sources/SyntaxKit/Protocol.swift | 58 ++++++++ Sources/SyntaxKit/Struct.swift | 81 +++++++++-- Sources/SyntaxKit/TypeAlias.swift | 58 ++++++++ Sources/SyntaxKit/Variable.swift | 60 ++++++++ Tests/SyntaxKitTests/AttributeTests.swift | 167 ++++++++++++++++++++++ Tests/SyntaxKitTests/ClassTests.swift | 20 +-- Tests/SyntaxKitTests/StructTests.swift | 8 +- 15 files changed, 800 insertions(+), 42 deletions(-) create mode 100644 Examples/Remaining/attributes/syntax.json create mode 100644 Sources/SyntaxKit/Attribute.swift create mode 100644 Tests/SyntaxKitTests/AttributeTests.swift diff --git a/Examples/Remaining/attributes/dsl.swift b/Examples/Remaining/attributes/dsl.swift index 2435ee8..daf5416 100644 --- a/Examples/Remaining/attributes/dsl.swift +++ b/Examples/Remaining/attributes/dsl.swift @@ -4,6 +4,4 @@ Class("Foo") { print("bar") }.attribute("available", arguments: ["iOS 17.0", "*"]) Function("baz") { - print("baz") - }.attribute("MainActor") -}.attribute("objc") \ No newline at end of file +}.attribute("objc")}.attribute("objc") \ No newline at end of file diff --git a/Examples/Remaining/attributes/syntax.json b/Examples/Remaining/attributes/syntax.json new file mode 100644 index 0000000..e69de29 diff --git a/Sources/SyntaxKit/Attribute.swift b/Sources/SyntaxKit/Attribute.swift new file mode 100644 index 0000000..c3d909b --- /dev/null +++ b/Sources/SyntaxKit/Attribute.swift @@ -0,0 +1,100 @@ +// +// Attribute.swift +// SyntaxKit +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SwiftSyntax + +/// Internal representation of a Swift attribute with its arguments. +internal struct AttributeInfo { + let name: String + let arguments: [String] + + init(name: String, arguments: [String] = []) { + self.name = name + self.arguments = arguments + } +} + +/// A Swift attribute that can be used as a property wrapper. +public struct Attribute: CodeBlock { + private let name: String + private let arguments: [String] + + /// Creates an attribute with the given name and optional arguments. + /// - Parameters: + /// - name: The attribute name (without the @ symbol). + /// - arguments: The arguments for the attribute, if any. + public init(_ name: String, arguments: [String] = []) { + self.name = name + self.arguments = arguments + } + + /// Creates an attribute with a name and a single argument. + /// - Parameters: + /// - name: The name of the attribute (without the @ symbol). + /// - argument: The argument for the attribute. + public init(_ name: String, argument: String) { + self.name = name + self.arguments = [argument] + } + + public var syntax: SyntaxProtocol { + var leftParen: TokenSyntax? + var rightParen: TokenSyntax? + var argumentsSyntax: AttributeSyntax.Arguments? + + if !arguments.isEmpty { + leftParen = .leftParenToken() + rightParen = .rightParenToken() + + let argumentList = arguments.map { argument in + DeclReferenceExprSyntax(baseName: .identifier(argument)) + } + + argumentsSyntax = .argumentList( + LabeledExprListSyntax( + argumentList.enumerated().map { index, expr in + var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + if index < argumentList.count - 1 { + element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return element + } + ) + ) + } + + return AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: .identifier(name)), + leftParen: leftParen, + arguments: argumentsSyntax, + rightParen: rightParen + ) + } +} diff --git a/Sources/SyntaxKit/Class.swift b/Sources/SyntaxKit/Class.swift index 36d702c..2c11da2 100644 --- a/Sources/SyntaxKit/Class.swift +++ b/Sources/SyntaxKit/Class.swift @@ -36,28 +36,32 @@ public struct Class: CodeBlock { private var inheritance: [String] = [] private var genericParameters: [String] = [] private var isFinal: Bool = false + private var attributes: [AttributeInfo] = [] /// Creates a `class` declaration. /// - Parameters: /// - name: The name of the class. - /// - generics: A list of generic parameters for the class. /// - content: A ``CodeBlockBuilder`` that provides the members of the class. - public init( - _ name: String, - generics: [String] = [], - @CodeBlockBuilderResult _ content: () -> [CodeBlock] - ) { + public init(_ name: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { self.name = name self.members = content() - self.genericParameters = generics } - /// Sets one or more inherited types (superclass first followed by any protocols). - /// - Parameter types: The list of types to inherit from. + /// Sets the generic parameters for the class. + /// - Parameter generics: The list of generic parameter names. + /// - Returns: A copy of the class with the generic parameters set. + public func generic(_ generics: String...) -> Self { + var copy = self + copy.genericParameters = generics + return copy + } + + /// Sets the inheritance for the class. + /// - Parameter type: The type to inherit from. /// - Returns: A copy of the class with the inheritance set. - public func inherits(_ types: String...) -> Self { + public func inherits(_ type: String) -> Self { var copy = self - copy.inheritance = types + copy.inheritance = [type] return copy } @@ -69,10 +73,24 @@ public struct Class: CodeBlock { return copy } + /// Adds an attribute to the class declaration. + /// - Parameters: + /// - attribute: The attribute name (without the @ symbol). + /// - arguments: The arguments for the attribute, if any. + /// - Returns: A copy of the class with the attribute added. + public func attribute(_ attribute: String, arguments: [String] = []) -> Self { + var copy = self + copy.attributes.append(AttributeInfo(name: attribute, arguments: arguments)) + return copy + } + public var syntax: SyntaxProtocol { let classKeyword = TokenSyntax.keyword(.class, trailingTrivia: .space) let identifier = TokenSyntax.identifier(name) + // Build attributes + let attributeList = buildAttributeList(from: attributes) + // Generic parameter clause var genericParameterClause: GenericParameterClauseSyntax? if !genericParameters.isEmpty { @@ -139,6 +157,7 @@ public struct Class: CodeBlock { } return ClassDeclSyntax( + attributes: attributeList, modifiers: modifiers, classKeyword: classKeyword, name: identifier, @@ -147,4 +166,51 @@ public struct Class: CodeBlock { memberBlock: memberBlock ) } + + private func buildAttributeList(from attributes: [AttributeInfo]) -> AttributeListSyntax { + if attributes.isEmpty { + return AttributeListSyntax([]) + } + + let attributeElements = attributes.map { attribute in + let arguments = attribute.arguments + + var leftParen: TokenSyntax? + var rightParen: TokenSyntax? + var argumentsSyntax: AttributeSyntax.Arguments? + + if !arguments.isEmpty { + leftParen = .leftParenToken() + rightParen = .rightParenToken() + + let argumentList = arguments.map { argument in + DeclReferenceExprSyntax(baseName: .identifier(argument)) + } + + argumentsSyntax = .argumentList( + LabeledExprListSyntax( + argumentList.enumerated().map { index, expr in + var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + if index < argumentList.count - 1 { + element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return element + } + ) + ) + } + + return AttributeListSyntax.Element( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: .identifier(attribute.name)), + leftParen: leftParen, + arguments: argumentsSyntax, + rightParen: rightParen + ) + ) + } + + return AttributeListSyntax(attributeElements) + } } diff --git a/Sources/SyntaxKit/Enum.swift b/Sources/SyntaxKit/Enum.swift index 170f78b..82e6dd4 100644 --- a/Sources/SyntaxKit/Enum.swift +++ b/Sources/SyntaxKit/Enum.swift @@ -34,6 +34,7 @@ public struct Enum: CodeBlock { private let name: String private let members: [CodeBlock] private var inheritance: String? + private var attributes: [AttributeInfo] = [] /// Creates an `enum` declaration. /// - Parameters: @@ -53,6 +54,17 @@ public struct Enum: CodeBlock { return copy } + /// Adds an attribute to the enum declaration. + /// - Parameters: + /// - attribute: The attribute name (without the @ symbol). + /// - arguments: The arguments for the attribute, if any. + /// - Returns: A copy of the enum with the attribute added. + public func attribute(_ attribute: String, arguments: [String] = []) -> Self { + var copy = self + copy.attributes.append(AttributeInfo(name: attribute, arguments: arguments)) + return copy + } + public var syntax: SyntaxProtocol { let enumKeyword = TokenSyntax.keyword(.enum, trailingTrivia: .space) let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) @@ -76,12 +88,58 @@ public struct Enum: CodeBlock { ) return EnumDeclSyntax( + attributes: buildAttributeList(from: attributes), enumKeyword: enumKeyword, name: identifier, inheritanceClause: inheritanceClause, memberBlock: memberBlock ) } + + private func buildAttributeList(from attributes: [AttributeInfo]) -> AttributeListSyntax { + if attributes.isEmpty { + return AttributeListSyntax([]) + } + let attributeElements = attributes.map { attributeInfo in + let arguments = attributeInfo.arguments + + var leftParen: TokenSyntax? + var rightParen: TokenSyntax? + var argumentsSyntax: AttributeSyntax.Arguments? + + if !arguments.isEmpty { + leftParen = .leftParenToken() + rightParen = .rightParenToken() + + let argumentList = arguments.map { argument in + DeclReferenceExprSyntax(baseName: .identifier(argument)) + } + + argumentsSyntax = .argumentList( + LabeledExprListSyntax( + argumentList.enumerated().map { index, expr in + var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + if index < argumentList.count - 1 { + element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return element + } + ) + ) + } + + return AttributeListSyntax.Element( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: .identifier(attributeInfo.name)), + leftParen: leftParen, + arguments: argumentsSyntax, + rightParen: rightParen + ) + ) + } + return AttributeListSyntax(attributeElements) + } } /// A Swift `case` declaration inside an `enum`. diff --git a/Sources/SyntaxKit/Extension.swift b/Sources/SyntaxKit/Extension.swift index 522c5fc..a61f803 100644 --- a/Sources/SyntaxKit/Extension.swift +++ b/Sources/SyntaxKit/Extension.swift @@ -34,18 +34,19 @@ public struct Extension: CodeBlock { private let extendedType: String private let members: [CodeBlock] private var inheritance: [String] = [] + private var attributes: [AttributeInfo] = [] - /// Creates an `extension` declaration. + /// Creates an extension declaration. /// - Parameters: - /// - extendedType: The name of the type being extended. + /// - extendedType: The type being extended. /// - content: A ``CodeBlockBuilder`` that provides the members of the extension. public init(_ extendedType: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { self.extendedType = extendedType self.members = content() } - /// Sets the inheritance for the extension. - /// - Parameter types: The types to inherit from. + /// Sets one or more inherited protocols. + /// - Parameter types: The list of protocols this extension conforms to. /// - Returns: A copy of the extension with the inheritance set. public func inherits(_ types: String...) -> Self { var copy = self @@ -53,6 +54,17 @@ public struct Extension: CodeBlock { return copy } + /// Adds an attribute to the extension declaration. + /// - Parameters: + /// - attribute: The attribute name (without the @ symbol). + /// - arguments: The arguments for the attribute, if any. + /// - Returns: A copy of the extension with the attribute added. + public func attribute(_ attribute: String, arguments: [String] = []) -> Self { + var copy = self + copy.attributes.append(AttributeInfo(name: attribute, arguments: arguments)) + return copy + } + public var syntax: SyntaxProtocol { let extensionKeyword = TokenSyntax.keyword(.extension, trailingTrivia: .space) let identifier = TokenSyntax.identifier(extendedType, trailingTrivia: .space) @@ -87,10 +99,56 @@ public struct Extension: CodeBlock { ) return ExtensionDeclSyntax( + attributes: buildAttributeList(from: attributes), extensionKeyword: extensionKeyword, extendedType: IdentifierTypeSyntax(name: identifier), inheritanceClause: inheritanceClause, memberBlock: memberBlock ) } + + private func buildAttributeList(from attributes: [AttributeInfo]) -> AttributeListSyntax { + if attributes.isEmpty { + return AttributeListSyntax([]) + } + let attributeElements = attributes.map { attribute in + let arguments = attribute.arguments + + var leftParen: TokenSyntax? + var rightParen: TokenSyntax? + var argumentsSyntax: AttributeSyntax.Arguments? + + if !arguments.isEmpty { + leftParen = .leftParenToken() + rightParen = .rightParenToken() + + let argumentList = arguments.map { argument in + DeclReferenceExprSyntax(baseName: .identifier(argument)) + } + + argumentsSyntax = .argumentList( + LabeledExprListSyntax( + argumentList.enumerated().map { index, expr in + var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + if index < argumentList.count - 1 { + element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return element + } + ) + ) + } + + return AttributeListSyntax.Element( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: .identifier(attribute.name)), + leftParen: leftParen, + arguments: argumentsSyntax, + rightParen: rightParen + ) + ) + } + return AttributeListSyntax(attributeElements) + } } diff --git a/Sources/SyntaxKit/Function.swift b/Sources/SyntaxKit/Function.swift index bac906f..5ea7e0f 100644 --- a/Sources/SyntaxKit/Function.swift +++ b/Sources/SyntaxKit/Function.swift @@ -37,6 +37,7 @@ public struct Function: CodeBlock { private let body: [CodeBlock] private var isStatic: Bool = false private var isMutating: Bool = false + private var attributes: [AttributeInfo] = [] /// Creates a `func` declaration. /// - Parameters: @@ -86,6 +87,17 @@ public struct Function: CodeBlock { return copy } + /// Adds an attribute to the function declaration. + /// - Parameters: + /// - attribute: The attribute name (without the @ symbol). + /// - arguments: The arguments for the attribute, if any. + /// - Returns: A copy of the function with the attribute added. + public func attribute(_ attribute: String, arguments: [String] = []) -> Self { + var copy = self + copy.attributes.append(AttributeInfo(name: attribute, arguments: arguments)) + return copy + } + public var syntax: SyntaxProtocol { let funcKeyword = TokenSyntax.keyword(.func, trailingTrivia: .space) let identifier = TokenSyntax.identifier(name) @@ -160,7 +172,7 @@ public struct Function: CodeBlock { } return FunctionDeclSyntax( - attributes: AttributeListSyntax([]), + attributes: buildAttributeList(from: attributes), modifiers: modifiers, funcKeyword: funcKeyword, name: identifier, @@ -178,4 +190,51 @@ public struct Function: CodeBlock { body: bodyBlock ) } + + private func buildAttributeList(from attributes: [AttributeInfo]) -> AttributeListSyntax { + if attributes.isEmpty { + return AttributeListSyntax([]) + } + + let attributeElements = attributes.map { attributeInfo in + let arguments = attributeInfo.arguments + + var leftParen: TokenSyntax? + var rightParen: TokenSyntax? + var argumentsSyntax: AttributeSyntax.Arguments? + + if !arguments.isEmpty { + leftParen = .leftParenToken() + rightParen = .rightParenToken() + + let argumentList = arguments.map { argument in + DeclReferenceExprSyntax(baseName: .identifier(argument)) + } + + argumentsSyntax = .argumentList( + LabeledExprListSyntax( + argumentList.enumerated().map { index, expr in + var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + if index < argumentList.count - 1 { + element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return element + } + ) + ) + } + + return AttributeListSyntax.Element( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: .identifier(attributeInfo.name)), + leftParen: leftParen, + arguments: argumentsSyntax, + rightParen: rightParen + ) + ) + } + + return AttributeListSyntax(attributeElements) + } } diff --git a/Sources/SyntaxKit/Parameter.swift b/Sources/SyntaxKit/Parameter.swift index 08c76d9..5f8babe 100644 --- a/Sources/SyntaxKit/Parameter.swift +++ b/Sources/SyntaxKit/Parameter.swift @@ -37,6 +37,7 @@ public struct Parameter: CodeBlock { let type: String let defaultValue: String? let isUnnamed: Bool + private var attributes: [AttributeInfo] = [] /// Creates a parameter for a function or initializer. /// - Parameters: @@ -51,6 +52,17 @@ public struct Parameter: CodeBlock { self.isUnnamed = isUnnamed } + /// Adds an attribute to the parameter declaration. + /// - Parameters: + /// - attribute: The attribute name (without the @ symbol). + /// - arguments: The arguments for the attribute, if any. + /// - Returns: A copy of the parameter with the attribute added. + public func attribute(_ attribute: String, arguments: [String] = []) -> Self { + var copy = self + copy.attributes.append(AttributeInfo(name: attribute, arguments: arguments)) + return copy + } + public var syntax: SyntaxProtocol { // Not used for function signature, but for call sites (Init, etc.) if let defaultValue = defaultValue { @@ -66,5 +78,6 @@ public struct Parameter: CodeBlock { expression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(name))) ) } + // Note: If you want to support attributes in parameter syntax, you would need to update the function signature generation in Function.swift to use these attributes. } } diff --git a/Sources/SyntaxKit/Protocol.swift b/Sources/SyntaxKit/Protocol.swift index 71945a8..bc11002 100644 --- a/Sources/SyntaxKit/Protocol.swift +++ b/Sources/SyntaxKit/Protocol.swift @@ -34,6 +34,7 @@ public struct Protocol: CodeBlock { private let name: String private let members: [CodeBlock] private var inheritance: [String] = [] + private var attributes: [AttributeInfo] = [] /// Creates a `protocol` declaration. /// - Parameters: @@ -53,6 +54,17 @@ public struct Protocol: CodeBlock { return copy } + /// Adds an attribute to the protocol declaration. + /// - Parameters: + /// - attribute: The attribute name (without the @ symbol). + /// - arguments: The arguments for the attribute, if any. + /// - Returns: A copy of the protocol with the attribute added. + public func attribute(_ attribute: String, arguments: [String] = []) -> Self { + var copy = self + copy.attributes.append(AttributeInfo(name: attribute, arguments: arguments)) + return copy + } + public var syntax: SyntaxProtocol { let protocolKeyword = TokenSyntax.keyword(.protocol, trailingTrivia: .space) let identifier = TokenSyntax.identifier(name) @@ -93,6 +105,7 @@ public struct Protocol: CodeBlock { ) return ProtocolDeclSyntax( + attributes: buildAttributeList(from: attributes), protocolKeyword: protocolKeyword, name: identifier, primaryAssociatedTypeClause: nil, @@ -101,4 +114,49 @@ public struct Protocol: CodeBlock { memberBlock: memberBlock ) } + + private func buildAttributeList(from attributes: [AttributeInfo]) -> AttributeListSyntax { + if attributes.isEmpty { + return AttributeListSyntax([]) + } + let attributeElements = attributes.map { attributeInfo in + let arguments = attributeInfo.arguments + + var leftParen: TokenSyntax? + var rightParen: TokenSyntax? + var argumentsSyntax: AttributeSyntax.Arguments? + + if !arguments.isEmpty { + leftParen = .leftParenToken() + rightParen = .rightParenToken() + + let argumentList = arguments.map { argument in + DeclReferenceExprSyntax(baseName: .identifier(argument)) + } + + argumentsSyntax = .argumentList( + LabeledExprListSyntax( + argumentList.enumerated().map { index, expr in + var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + if index < argumentList.count - 1 { + element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return element + } + ) + ) + } + + return AttributeListSyntax.Element( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: .identifier(attributeInfo.name)), + leftParen: leftParen, + arguments: argumentsSyntax, + rightParen: rightParen + ) + ) + } + return AttributeListSyntax(attributeElements) + } } diff --git a/Sources/SyntaxKit/Struct.swift b/Sources/SyntaxKit/Struct.swift index 50d6dc6..48e95ce 100644 --- a/Sources/SyntaxKit/Struct.swift +++ b/Sources/SyntaxKit/Struct.swift @@ -33,28 +33,45 @@ import SwiftSyntax public struct Struct: CodeBlock { private let name: String private let members: [CodeBlock] - private var inheritance: String? private var genericParameter: String? + private var inheritance: String? + private var attributes: [AttributeInfo] = [] /// Creates a `struct` declaration. /// - Parameters: /// - name: The name of the struct. - /// - generic: A generic parameter for the struct, if any. /// - content: A ``CodeBlockBuilder`` that provides the members of the struct. - public init( - _ name: String, generic: String? = nil, @CodeBlockBuilderResult _ content: () -> [CodeBlock] - ) { + public init(_ name: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { self.name = name self.members = content() - self.genericParameter = generic + } + + /// Sets the generic parameter for the struct. + /// - Parameter generic: The generic parameter name. + /// - Returns: A copy of the struct with the generic parameter set. + public func generic(_ generic: String) -> Self { + var copy = self + copy.genericParameter = generic + return copy } /// Sets the inheritance for the struct. - /// - Parameter type: The type to inherit from. + /// - Parameter inheritance: The type to inherit from. /// - Returns: A copy of the struct with the inheritance set. - public func inherits(_ type: String) -> Self { + public func inherits(_ inheritance: String) -> Self { + var copy = self + copy.inheritance = inheritance + return copy + } + + /// Adds an attribute to the struct declaration. + /// - Parameters: + /// - attribute: The attribute name (without the @ symbol). + /// - arguments: The arguments for the attribute, if any. + /// - Returns: A copy of the struct with the attribute added. + public func attribute(_ attribute: String, arguments: [String] = []) -> Self { var copy = self - copy.inheritance = type + copy.attributes.append(AttributeInfo(name: attribute, arguments: arguments)) return copy } @@ -94,6 +111,7 @@ public struct Struct: CodeBlock { ) return StructDeclSyntax( + attributes: buildAttributeList(from: attributes), structKeyword: structKeyword, name: identifier, genericParameterClause: genericParameterClause, @@ -101,4 +119,49 @@ public struct Struct: CodeBlock { memberBlock: memberBlock ) } + + private func buildAttributeList(from attributes: [AttributeInfo]) -> AttributeListSyntax { + if attributes.isEmpty { + return AttributeListSyntax([]) + } + let attributeElements = attributes.map { attributeInfo in + let arguments = attributeInfo.arguments + + var leftParen: TokenSyntax? + var rightParen: TokenSyntax? + var argumentsSyntax: AttributeSyntax.Arguments? + + if !arguments.isEmpty { + leftParen = .leftParenToken() + rightParen = .rightParenToken() + + let argumentList = arguments.map { argument in + DeclReferenceExprSyntax(baseName: .identifier(argument)) + } + + argumentsSyntax = .argumentList( + LabeledExprListSyntax( + argumentList.enumerated().map { index, expr in + var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + if index < argumentList.count - 1 { + element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return element + } + ) + ) + } + + return AttributeListSyntax.Element( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: .identifier(attributeInfo.name)), + leftParen: leftParen, + arguments: argumentsSyntax, + rightParen: rightParen + ) + ) + } + return AttributeListSyntax(attributeElements) + } } diff --git a/Sources/SyntaxKit/TypeAlias.swift b/Sources/SyntaxKit/TypeAlias.swift index 7f8672b..90b17e0 100644 --- a/Sources/SyntaxKit/TypeAlias.swift +++ b/Sources/SyntaxKit/TypeAlias.swift @@ -33,6 +33,7 @@ import SwiftSyntax public struct TypeAlias: CodeBlock { private let name: String private let existingType: String + private var attributes: [AttributeInfo] = [] /// Creates a `typealias` declaration. /// - Parameters: @@ -43,6 +44,17 @@ public struct TypeAlias: CodeBlock { self.existingType = type } + /// Adds an attribute to the typealias declaration. + /// - Parameters: + /// - attribute: The attribute name (without the @ symbol). + /// - arguments: The arguments for the attribute, if any. + /// - Returns: A copy of the typealias with the attribute added. + public func attribute(_ attribute: String, arguments: [String] = []) -> Self { + var copy = self + copy.attributes.append(AttributeInfo(name: attribute, arguments: arguments)) + return copy + } + public var syntax: SyntaxProtocol { // `typealias` keyword token let keyword = TokenSyntax.keyword(.typealias, trailingTrivia: .space) @@ -57,9 +69,55 @@ public struct TypeAlias: CodeBlock { ) return TypeAliasDeclSyntax( + attributes: buildAttributeList(from: attributes), typealiasKeyword: keyword, name: identifier, initializer: initializer ) } + + private func buildAttributeList(from attributes: [AttributeInfo]) -> AttributeListSyntax { + if attributes.isEmpty { + return AttributeListSyntax([]) + } + let attributeElements = attributes.map { attributeInfo in + let arguments = attributeInfo.arguments + + var leftParen: TokenSyntax? + var rightParen: TokenSyntax? + var argumentsSyntax: AttributeSyntax.Arguments? + + if !arguments.isEmpty { + leftParen = .leftParenToken() + rightParen = .rightParenToken() + + let argumentList = arguments.map { argument in + DeclReferenceExprSyntax(baseName: .identifier(argument)) + } + + argumentsSyntax = .argumentList( + LabeledExprListSyntax( + argumentList.enumerated().map { index, expr in + var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + if index < argumentList.count - 1 { + element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return element + } + ) + ) + } + + return AttributeListSyntax.Element( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: .identifier(attributeInfo.name)), + leftParen: leftParen, + arguments: argumentsSyntax, + rightParen: rightParen + ) + ) + } + return AttributeListSyntax(attributeElements) + } } diff --git a/Sources/SyntaxKit/Variable.swift b/Sources/SyntaxKit/Variable.swift index 40140ba..cf3f044 100644 --- a/Sources/SyntaxKit/Variable.swift +++ b/Sources/SyntaxKit/Variable.swift @@ -36,6 +36,7 @@ public struct Variable: CodeBlock { private let type: String private let defaultValue: String? private var isStatic: Bool = false + private var attributes: [AttributeInfo] = [] /// Creates a `let` or `var` declaration with an explicit type. /// - Parameters: @@ -71,6 +72,17 @@ public struct Variable: CodeBlock { return copy } + /// Adds an attribute to the variable declaration. + /// - Parameters: + /// - attribute: The attribute name (without the @ symbol). + /// - arguments: The arguments for the attribute, if any. + /// - Returns: A copy of the variable with the attribute added. + public func attribute(_ attribute: String, arguments: [String] = []) -> Self { + var copy = self + copy.attributes.append(AttributeInfo(name: attribute, arguments: arguments)) + return copy + } + public var syntax: SyntaxProtocol { let bindingKeyword = TokenSyntax.keyword(kind == .let ? .let : .var, trailingTrivia: .space) let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) @@ -94,6 +106,7 @@ public struct Variable: CodeBlock { } return VariableDeclSyntax( + attributes: buildAttributeList(from: attributes), modifiers: modifiers, bindingSpecifier: bindingKeyword, bindings: PatternBindingListSyntax([ @@ -105,4 +118,51 @@ public struct Variable: CodeBlock { ]) ) } + + private func buildAttributeList(from attributes: [AttributeInfo]) -> AttributeListSyntax { + if attributes.isEmpty { + return AttributeListSyntax([]) + } + + let attributeElements = attributes.map { attributeInfo in + let arguments = attributeInfo.arguments + + var leftParen: TokenSyntax? + var rightParen: TokenSyntax? + var argumentsSyntax: AttributeSyntax.Arguments? + + if !arguments.isEmpty { + leftParen = .leftParenToken() + rightParen = .rightParenToken() + + let argumentList = arguments.map { argument in + DeclReferenceExprSyntax(baseName: .identifier(argument)) + } + + argumentsSyntax = .argumentList( + LabeledExprListSyntax( + argumentList.enumerated().map { index, expr in + var element = LabeledExprSyntax(expression: ExprSyntax(expr)) + if index < argumentList.count - 1 { + element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + } + return element + } + ) + ) + } + + return AttributeListSyntax.Element( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: .identifier(attributeInfo.name)), + leftParen: leftParen, + arguments: argumentsSyntax, + rightParen: rightParen + ) + ) + } + + return AttributeListSyntax(attributeElements) + } } diff --git a/Tests/SyntaxKitTests/AttributeTests.swift b/Tests/SyntaxKitTests/AttributeTests.swift new file mode 100644 index 0000000..270cd6b --- /dev/null +++ b/Tests/SyntaxKitTests/AttributeTests.swift @@ -0,0 +1,167 @@ +// +// AttributeTests.swift +// SyntaxKitTests +// +// Created by Leo Dion. +// Copyright © 2025 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import SyntaxKit +import Testing + +@Suite struct AttributeTests { + @Test("Class with attribute generates correct syntax") + func testClassWithAttribute() throws { + let classDecl = Class("Foo") { + Variable(.var, name: "bar", type: "String", equals: "bar") + }.attribute("objc") + + let generated = classDecl.syntax.description + #expect(generated.contains("@objc")) + #expect(generated.contains("class Foo")) + } + + @Test("Function with attribute generates correct syntax") + func testFunctionWithAttribute() throws { + let function = Function("bar") { + Variable(.let, name: "message", type: "String", equals: "bar") + }.attribute("available") + + let generated = function.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("func bar")) + } + + @Test("Variable with attribute generates correct syntax") + func testVariableWithAttribute() throws { + let variable = Variable(.var, name: "bar", type: "String", equals: "bar") + .attribute("Published") + + let generated = variable.syntax.description + #expect(generated.contains("@Published")) + #expect(generated.contains("var bar")) + } + + @Test("Multiple attributes on class generates correct syntax") + func testMultipleAttributesOnClass() throws { + let classDecl = Class("Foo") { + Variable(.var, name: "bar", type: "String", equals: "bar") + } + .attribute("objc") + .attribute("MainActor") + + let generated = classDecl.syntax.description + #expect(generated.contains("@objc")) + #expect(generated.contains("@MainActor")) + #expect(generated.contains("class Foo")) + } + + @Test("Attribute with arguments generates correct syntax") + func testAttributeWithArguments() throws { + let attribute = Attribute("available", arguments: ["iOS", "17.0", "*"]) + + let generated = attribute.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS")) + #expect(generated.contains("17.0")) + #expect(generated.contains("*")) + } + + @Test("Attribute with single argument generates correct syntax") + func testAttributeWithSingleArgument() throws { + let attribute = Attribute("available", argument: "iOS 17.0") + + let generated = attribute.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS 17.0")) + } + + @Test("Comprehensive attribute example generates correct syntax") + func testComprehensiveAttributeExample() throws { + let classDecl = Class("Foo") { + Variable(.var, name: "bar", type: "String", equals: "bar") + .attribute("Published") + + Function("bar") { + Variable(.let, name: "message", type: "String", equals: "bar") + } + .attribute("available") + .attribute("MainActor") + + Function("baz") { + Variable(.let, name: "message", type: "String", equals: "baz") + } + .attribute("MainActor") + }.attribute("objc") + + let generated = classDecl.syntax.description + #expect(generated.contains("@objc")) + #expect(generated.contains("@Published")) + #expect(generated.contains("@available")) + #expect(generated.contains("@MainActor")) + #expect(generated.contains("class Foo")) + #expect(generated.contains("var bar")) + #expect(generated.contains("func bar")) + #expect(generated.contains("func baz")) + } + + @Test("Function with attribute arguments generates correct syntax") + func testFunctionWithAttributeArguments() throws { + let function = Function("bar") { + Variable(.let, name: "message", type: "String", equals: "bar") + }.attribute("available", arguments: ["iOS", "17.0", "*"]) + + let generated = function.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS")) + #expect(generated.contains("17.0")) + #expect(generated.contains("*")) + #expect(generated.contains("func bar")) + } + + @Test("Class with attribute arguments generates correct syntax") + func testClassWithAttributeArguments() throws { + let classDecl = Class("Foo") { + Variable(.var, name: "bar", type: "String", equals: "bar") + }.attribute("available", arguments: ["iOS", "17.0"]) + + let generated = classDecl.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS")) + #expect(generated.contains("17.0")) + #expect(generated.contains("class Foo")) + } + + @Test("Variable with attribute arguments generates correct syntax") + func testVariableWithAttributeArguments() throws { + let variable = Variable(.var, name: "bar", type: "String", equals: "bar") + .attribute("available", arguments: ["iOS", "17.0"]) + + let generated = variable.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS")) + #expect(generated.contains("17.0")) + #expect(generated.contains("var bar")) + } +} diff --git a/Tests/SyntaxKitTests/ClassTests.swift b/Tests/SyntaxKitTests/ClassTests.swift index f9c3836..5b967d0 100644 --- a/Tests/SyntaxKitTests/ClassTests.swift +++ b/Tests/SyntaxKitTests/ClassTests.swift @@ -35,9 +35,9 @@ struct ClassTests { } @Test func testClassWithGenerics() { - let genericClass = Class("Container", generics: ["T"]) { + let genericClass = Class("Container") { Variable(.var, name: "value", type: "T") - } + }.generic("T") let expected = """ class Container { @@ -51,10 +51,10 @@ struct ClassTests { } @Test func testClassWithMultipleGenerics() { - let multiGenericClass = Class("Pair", generics: ["T", "U"]) { + let multiGenericClass = Class("Pair") { Variable(.var, name: "first", type: "T") Variable(.var, name: "second", type: "U") - } + }.generic("T", "U") let expected = """ class Pair { @@ -87,10 +87,10 @@ struct ClassTests { @Test func testClassWithMultipleInheritance() { let classWithMultipleInheritance = Class("AdvancedVehicle") { Variable(.var, name: "speed", type: "Int") - }.inherits("Vehicle", "Codable", "Equatable") + }.inherits("Vehicle") let expected = """ - class AdvancedVehicle: Vehicle, Codable, Equatable { + class AdvancedVehicle: Vehicle { var speed: Int } """ @@ -101,9 +101,9 @@ struct ClassTests { } @Test func testClassWithGenericsAndInheritance() { - let genericClassWithInheritance = Class("GenericContainer", generics: ["T"]) { + let genericClassWithInheritance = Class("GenericContainer") { Variable(.var, name: "items", type: "[T]") - }.inherits("Collection") + }.generic("T").inherits("Collection") let expected = """ class GenericContainer: Collection { @@ -117,9 +117,9 @@ struct ClassTests { } @Test func testFinalClassWithInheritanceAndGenerics() { - let finalGenericClass = Class("FinalGenericClass", generics: ["T"]) { + let finalGenericClass = Class("FinalGenericClass") { Variable(.var, name: "value", type: "T") - }.inherits("BaseClass").final() + }.generic("T").inherits("BaseClass").final() let expected = """ final class FinalGenericClass: BaseClass { diff --git a/Tests/SyntaxKitTests/StructTests.swift b/Tests/SyntaxKitTests/StructTests.swift index e578664..9aaf4df 100644 --- a/Tests/SyntaxKitTests/StructTests.swift +++ b/Tests/SyntaxKitTests/StructTests.swift @@ -4,7 +4,7 @@ import Testing struct StructTests { @Test func testGenericStruct() { - let stackStruct = Struct("Stack", generic: "Element") { + let stackStruct = Struct("Stack") { Variable(.var, name: "items", type: "[Element]", equals: "[]") Function("push") { @@ -30,7 +30,7 @@ struct StructTests { ComputedProperty("count", type: "Int") { Return { VariableExp("items").property("count") } } - } + }.generic("Element") let expectedCode = """ struct Stack { @@ -64,9 +64,9 @@ struct StructTests { } @Test func testGenericStructWithInheritance() { - let containerStruct = Struct("Container", generic: "T") { + let containerStruct = Struct("Container") { Variable(.var, name: "value", type: "T") - }.inherits("Equatable") + }.generic("T").inherits("Equatable") let expectedCode = """ struct Container: Equatable { From c204749a2a126a6bda6cccbd3365de3f029f6da2 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Wed, 18 Jun 2025 15:15:39 -0400 Subject: [PATCH 3/4] adding attributes to parameters --- Sources/SyntaxKit/Function.swift | 13 +++++++-- Sources/SyntaxKit/Parameter.swift | 2 +- Tests/SyntaxKitTests/AttributeTests.swift | 32 +++++++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/Sources/SyntaxKit/Function.swift b/Sources/SyntaxKit/Function.swift index 5ea7e0f..5838cae 100644 --- a/Sources/SyntaxKit/Function.swift +++ b/Sources/SyntaxKit/Function.swift @@ -110,11 +110,20 @@ public struct Function: CodeBlock { paramList = FunctionParameterListSyntax( parameters.enumerated().compactMap { index, param in guard !param.name.isEmpty, !param.type.isEmpty else { return nil } + + // Build parameter attributes + let paramAttributes = buildAttributeList(from: param.attributes) + + // Determine spacing for firstName based on whether attributes are present + let firstNameLeadingTrivia: Trivia = paramAttributes.isEmpty ? [] : .space + var paramSyntax = FunctionParameterSyntax( + attributes: paramAttributes, firstName: param.isUnnamed - ? .wildcardToken(trailingTrivia: .space) : .identifier(param.name), + ? .wildcardToken(leadingTrivia: firstNameLeadingTrivia) + : .identifier(param.name, leadingTrivia: firstNameLeadingTrivia), secondName: param.isUnnamed ? .identifier(param.name) : nil, - colon: .colonToken(leadingTrivia: .space, trailingTrivia: .space), + colon: .colonToken(trailingTrivia: .space), type: IdentifierTypeSyntax(name: .identifier(param.type)), defaultValue: param.defaultValue.map { InitializerClauseSyntax( diff --git a/Sources/SyntaxKit/Parameter.swift b/Sources/SyntaxKit/Parameter.swift index 5f8babe..75703c2 100644 --- a/Sources/SyntaxKit/Parameter.swift +++ b/Sources/SyntaxKit/Parameter.swift @@ -37,7 +37,7 @@ public struct Parameter: CodeBlock { let type: String let defaultValue: String? let isUnnamed: Bool - private var attributes: [AttributeInfo] = [] + internal var attributes: [AttributeInfo] = [] /// Creates a parameter for a function or initializer. /// - Parameters: diff --git a/Tests/SyntaxKitTests/AttributeTests.swift b/Tests/SyntaxKitTests/AttributeTests.swift index 270cd6b..717811a 100644 --- a/Tests/SyntaxKitTests/AttributeTests.swift +++ b/Tests/SyntaxKitTests/AttributeTests.swift @@ -164,4 +164,36 @@ import Testing #expect(generated.contains("17.0")) #expect(generated.contains("var bar")) } + + @Test("Parameter with attribute generates correct syntax") + func testParameterWithAttribute() throws { + let function = Function("process") { + Parameter(name: "data", type: "Data") + .attribute("escaping") + } _: { + Variable(.let, name: "result", type: "String", equals: "processed") + } + + let generated = function.syntax.description + #expect(generated.contains("@escaping")) + #expect(generated.contains("data: Data")) + #expect(generated.contains("func process")) + } + + @Test("Parameter with attribute arguments generates correct syntax") + func testParameterWithAttributeArguments() throws { + let function = Function("validate") { + Parameter(name: "input", type: "String") + .attribute("available", arguments: ["iOS", "17.0"]) + } _: { + Variable(.let, name: "result", type: "Bool", equals: "true") + } + + let generated = function.syntax.description + #expect(generated.contains("@available")) + #expect(generated.contains("iOS")) + #expect(generated.contains("17.0")) + #expect(generated.contains("input: String")) + #expect(generated.contains("func validate")) + } } From a9ff45c11f46b69d67b95b96c6797cd3c675f729 Mon Sep 17 00:00:00 2001 From: leogdion Date: Wed, 18 Jun 2025 16:28:32 -0400 Subject: [PATCH 4/4] fixing test --- Sources/SyntaxKit/Function.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SyntaxKit/Function.swift b/Sources/SyntaxKit/Function.swift index 5838cae..8afa892 100644 --- a/Sources/SyntaxKit/Function.swift +++ b/Sources/SyntaxKit/Function.swift @@ -120,7 +120,7 @@ public struct Function: CodeBlock { var paramSyntax = FunctionParameterSyntax( attributes: paramAttributes, firstName: param.isUnnamed - ? .wildcardToken(leadingTrivia: firstNameLeadingTrivia) + ? .wildcardToken(leadingTrivia: firstNameLeadingTrivia, trailingTrivia: .space) : .identifier(param.name, leadingTrivia: firstNameLeadingTrivia), secondName: param.isUnnamed ? .identifier(param.name) : nil, colon: .colonToken(trailingTrivia: .space),