Skip to content

Adding Attributes #72

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 4 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 14 additions & 0 deletions Examples/Remaining/attributes/code.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
7 changes: 7 additions & 0 deletions Examples/Remaining/attributes/dsl.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Class("Foo") {
Variable(.var, name: "bar", type: "String", defaultValue: "bar").attribute("Published")
Function("bar") {
print("bar")
}.attribute("available", arguments: ["iOS 17.0", "*"])
Function("baz") {
}.attribute("objc")}.attribute("objc")
Empty file.
100 changes: 100 additions & 0 deletions Sources/SyntaxKit/Attribute.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}
88 changes: 77 additions & 11 deletions Sources/SyntaxKit/Class.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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 {
Expand Down Expand Up @@ -139,6 +157,7 @@ public struct Class: CodeBlock {
}

return ClassDeclSyntax(
attributes: attributeList,
modifiers: modifiers,
classKeyword: classKeyword,
name: identifier,
Expand All @@ -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)
}
}
58 changes: 58 additions & 0 deletions Sources/SyntaxKit/Enum.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
private let name: String
private let members: [CodeBlock]
private var inheritance: String?
private var attributes: [AttributeInfo] = []

/// Creates an `enum` declaration.
/// - Parameters:
Expand All @@ -53,6 +54,17 @@
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
}

Check warning on line 66 in Sources/SyntaxKit/Enum.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SyntaxKit/Enum.swift#L62-L66

Added lines #L62 - L66 were not covered by tests

public var syntax: SyntaxProtocol {
let enumKeyword = TokenSyntax.keyword(.enum, trailingTrivia: .space)
let identifier = TokenSyntax.identifier(name, trailingTrivia: .space)
Expand All @@ -76,12 +88,58 @@
)

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)

Check warning on line 141 in Sources/SyntaxKit/Enum.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SyntaxKit/Enum.swift#L103-L141

Added lines #L103 - L141 were not covered by tests
}
}

/// A Swift `case` declaration inside an `enum`.
Expand Down
Loading
Loading