Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions Scripts/lint.sh
Comment thread
leogdion marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ fi

# Ensure mise-managed tools are on PATH outside CI (CI uses jdx/mise-action)
if command -v mise >/dev/null 2>&1 && [ -z "$CI" ]; then
# Trust the config (no-op if already trusted) so `mise env` doesn't silently
# emit nothing when mise.toml is new/untrusted, which would leave the
# mise-managed tools (swiftlint, periphery, ...) off PATH.
mise trust --quiet "$PACKAGE_DIR/mise.toml" >/dev/null 2>&1 || true
# Install any declared-but-missing tools so they resolve on PATH.
mise -C "$PACKAGE_DIR" install >/dev/null 2>&1 || true
eval "$(mise -C "$PACKAGE_DIR" env -s bash)"
fi

Expand Down
76 changes: 76 additions & 0 deletions Sources/SyntaxKit/Declarations/Class+Modifiers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// Class+Modifiers.swift
// SyntaxKit
//
// Created by Leo Dion.
// Copyright © 2026 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.
//

extension Class {
/// 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 inheritance: The types to inherit from.
/// - Returns: A copy of the class with the inheritance set.
public func inherits(_ inheritance: String...) -> Self {
var copy = self
copy.inheritance = inheritance
return copy
}

/// Marks the class declaration as `final`.
/// - Returns: A copy of the class marked as `final`.
public func final() -> Self {
var copy = self
copy.isFinal = true
return copy
}

/// Sets the access modifier for the class declaration.
/// - Parameter access: The access modifier.
/// - Returns: A copy of the class with the access modifier set.
public func access(_ access: AccessModifier) -> Self {
var copy = self
copy.accessModifier = access
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
}
}
64 changes: 17 additions & 47 deletions Sources/SyntaxKit/Declarations/Class.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ public import SwiftSyntax

/// A Swift `class` declaration.
public struct Class: CodeBlock, Sendable {
private let name: String
private let members: [any CodeBlock]
private var inheritance: [String] = []
private var genericParameters: [String] = []
private var isFinal: Bool = false
private var attributes: [AttributeInfo] = []
internal let name: String
internal let members: [any CodeBlock]
internal var inheritance: [String] = []
internal var genericParameters: [String] = []
internal var isFinal: Bool = false
internal var attributes: [AttributeInfo] = []
internal var accessModifier: AccessModifier?

/// The SwiftSyntax representation of this class declaration.
public var syntax: any SyntaxProtocol {
Expand Down Expand Up @@ -107,11 +108,16 @@ public struct Class: CodeBlock, Sendable {

// Modifiers
var modifiers: DeclModifierListSyntax = []
if isFinal {
if let access = accessModifier {
modifiers = DeclModifierListSyntax([
DeclModifierSyntax(name: .keyword(.final, trailingTrivia: .space))
DeclModifierSyntax(name: .keyword(access.keyword, trailingTrivia: .space))
])
}
if isFinal {
modifiers = DeclModifierListSyntax(
modifiers + [DeclModifierSyntax(name: .keyword(.final, trailingTrivia: .space))]
)
}

return ClassDeclSyntax(
attributes: attributeList,
Expand All @@ -135,43 +141,6 @@ public struct Class: CodeBlock, Sendable {
self.members = try content()
}

/// 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 inheritance: The types to inherit from.
/// - Returns: A copy of the class with the inheritance set.
public func inherits(_ inheritance: String...) -> Self {
var copy = self
copy.inheritance = inheritance
return copy
}

/// Marks the class declaration as `final`.
/// - Returns: A copy of the class marked as `final`.
public func final() -> Self {
var copy = self
copy.isFinal = true
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
}

private func buildAttributeList(from attributes: [AttributeInfo]) -> AttributeListSyntax {
if attributes.isEmpty {
return AttributeListSyntax([])
Expand All @@ -189,13 +158,13 @@ public struct Class: CodeBlock, Sendable {
rightParen = .rightParenToken()

let argumentList = arguments.map { argument in
DeclReferenceExprSyntax(baseName: .identifier(argument))
buildAttributeArgumentExpr(from: argument)
}

argumentsSyntax = .argumentList(
LabeledExprListSyntax(
argumentList.enumerated().map { index, expr in
var element = LabeledExprSyntax(expression: ExprSyntax(expr))
var element = LabeledExprSyntax(expression: expr)
if index < argumentList.count - 1 {
element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space))
}
Expand All @@ -213,6 +182,7 @@ public struct Class: CodeBlock, Sendable {
arguments: argumentsSyntax,
rightParen: rightParen
)
.with(\.trailingTrivia, .newline)
)
}

Expand Down
5 changes: 3 additions & 2 deletions Sources/SyntaxKit/Declarations/Enum.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,13 @@ public struct Enum: CodeBlock, Sendable {
rightParen = .rightParenToken()

let argumentList = arguments.map { argument in
DeclReferenceExprSyntax(baseName: .identifier(argument))
buildAttributeArgumentExpr(from: argument)
}

argumentsSyntax = .argumentList(
LabeledExprListSyntax(
argumentList.enumerated().map { index, expr in
var element = LabeledExprSyntax(expression: ExprSyntax(expr))
var element = LabeledExprSyntax(expression: expr)
if index < argumentList.count - 1 {
element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space))
}
Expand All @@ -158,6 +158,7 @@ public struct Enum: CodeBlock, Sendable {
arguments: argumentsSyntax,
rightParen: rightParen
)
.with(\.trailingTrivia, .newline)
)
}
return AttributeListSyntax(attributeElements)
Expand Down
5 changes: 3 additions & 2 deletions Sources/SyntaxKit/Declarations/Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,13 @@ public struct Extension: CodeBlock, Sendable {
rightParen = .rightParenToken()

let argumentList = arguments.map { argument in
DeclReferenceExprSyntax(baseName: .identifier(argument))
buildAttributeArgumentExpr(from: argument)
}

argumentsSyntax = .argumentList(
LabeledExprListSyntax(
argumentList.enumerated().map { index, expr in
var element = LabeledExprSyntax(expression: ExprSyntax(expr))
var element = LabeledExprSyntax(expression: expr)
if index < argumentList.count - 1 {
element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space))
}
Expand All @@ -155,6 +155,7 @@ public struct Extension: CodeBlock, Sendable {
arguments: argumentsSyntax,
rightParen: rightParen
)
.with(\.trailingTrivia, .newline)
)
}
return AttributeListSyntax(attributeElements)
Expand Down
78 changes: 78 additions & 0 deletions Sources/SyntaxKit/Declarations/IfCanImport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// IfCanImport.swift
// SyntaxKit
//
// Created by Leo Dion.
// Copyright © 2026 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.
//

public import SwiftSyntax

/// A `#if canImport(Module)` … `#endif` conditional compilation block.
public struct IfCanImport: CodeBlock, Sendable {
Comment thread
leogdion marked this conversation as resolved.
Outdated
private let moduleName: String
private let content: [any CodeBlock]

/// The SwiftSyntax representation of this conditional compilation block.
public var syntax: any SyntaxProtocol {
let canImportRef = DeclReferenceExprSyntax(baseName: .identifier("canImport"))
let moduleRef = DeclReferenceExprSyntax(baseName: .identifier(moduleName))
let condition = FunctionCallExprSyntax(
calledExpression: ExprSyntax(canImportRef),
leftParen: .leftParenToken(),
arguments: LabeledExprListSyntax([
LabeledExprSyntax(expression: ExprSyntax(moduleRef))
]),
rightParen: .rightParenToken()
)

let items = CodeBlockItemListSyntax(
content.compactMap { block -> CodeBlockItemSyntax? in
CodeBlockItemSyntax.Item.create(from: block.syntax).map {
CodeBlockItemSyntax(item: $0, trailingTrivia: .newline)
}
}
)

let clause = IfConfigClauseSyntax(
poundKeyword: .poundIfToken(trailingTrivia: .space),
condition: ExprSyntax(condition).with(\.trailingTrivia, .newline),
elements: .statements(items)
)

return IfConfigDeclSyntax(
clauses: IfConfigClauseListSyntax([clause]),
poundEndif: .poundEndifToken(leadingTrivia: .newline)
)
}

/// Creates a `#if canImport(moduleName)` block wrapping the given content.
/// - Parameters:
/// - moduleName: The module name passed to `canImport`.
/// - content: The code blocks to emit when the module is available.
public init(_ moduleName: String, @CodeBlockBuilderResult _ content: () -> [any CodeBlock]) {
self.moduleName = moduleName
self.content = content()
}
}
5 changes: 3 additions & 2 deletions Sources/SyntaxKit/Declarations/Import.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,13 @@ public struct Import: CodeBlock, Sendable {
rightParen = .rightParenToken()

let argumentList = arguments.map { argument in
DeclReferenceExprSyntax(baseName: .identifier(argument))
buildAttributeArgumentExpr(from: argument)
}

argumentsSyntax = .argumentList(
LabeledExprListSyntax(
argumentList.enumerated().map { index, expr in
var element = LabeledExprSyntax(expression: ExprSyntax(expr))
var element = LabeledExprSyntax(expression: expr)
if index < argumentList.count - 1 {
element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space))
}
Expand All @@ -125,6 +125,7 @@ public struct Import: CodeBlock, Sendable {
arguments: argumentsSyntax,
rightParen: rightParen
)
.with(\.trailingTrivia, .space)
)
}
return AttributeListSyntax(attributeElements)
Expand Down
Loading
Loading