diff --git a/.swiftlint.yml b/.swiftlint.yml index 91eed35..f9212ce 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -11,7 +11,6 @@ opt_in_rules: - contains_over_range_nil_comparison - convenience_type - discouraged_object_literal - - discouraged_optional_boolean - empty_collection_literal - empty_count - empty_string @@ -121,6 +120,7 @@ excluded: - Mint - Examples - Macros + - Sources/SyntaxKit/parser indentation_width: indentation_width: 2 file_name: diff --git a/Sources/SyntaxKit/Attribute.swift b/Sources/SyntaxKit/Attributes/Attribute.swift similarity index 100% rename from Sources/SyntaxKit/Attribute.swift rename to Sources/SyntaxKit/Attributes/Attribute.swift diff --git a/Sources/SyntaxKit/Trivia+Comments.swift b/Sources/SyntaxKit/Attributes/Trivia+Comments.swift similarity index 100% rename from Sources/SyntaxKit/Trivia+Comments.swift rename to Sources/SyntaxKit/Attributes/Trivia+Comments.swift diff --git a/Sources/SyntaxKit/CodeBlock+ExprSyntax.swift b/Sources/SyntaxKit/CodeBlocks/CodeBlock+ExprSyntax.swift similarity index 100% rename from Sources/SyntaxKit/CodeBlock+ExprSyntax.swift rename to Sources/SyntaxKit/CodeBlocks/CodeBlock+ExprSyntax.swift diff --git a/Sources/SyntaxKit/CodeBlock+Generate.swift b/Sources/SyntaxKit/CodeBlocks/CodeBlock+Generate.swift similarity index 63% rename from Sources/SyntaxKit/CodeBlock+Generate.swift rename to Sources/SyntaxKit/CodeBlocks/CodeBlock+Generate.swift index ecde6e4..adfd488 100644 --- a/Sources/SyntaxKit/CodeBlock+Generate.swift +++ b/Sources/SyntaxKit/CodeBlocks/CodeBlock+Generate.swift @@ -42,30 +42,12 @@ extension CodeBlock { statements = codeBlock.statements } else { let item: CodeBlockItemSyntax.Item - if let decl = self.syntax.as(DeclSyntax.self) { - item = .decl(decl) - } else if let stmt = self.syntax.as(StmtSyntax.self) { - item = .stmt(stmt) - } else if let expr = self.syntax.as(ExprSyntax.self) { - item = .expr(expr) - } else if let token = self.syntax.as(TokenSyntax.self) { - // Wrap TokenSyntax in DeclReferenceExprSyntax and then in ExprSyntax - let expr = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(token.text))) - item = .expr(expr) - } else if let switchCase = self.syntax.as(SwitchCaseSyntax.self) { - // Wrap SwitchCaseSyntax in a SwitchExprSyntax and treat it as an expression - // This is a fallback for when SwitchCase is used standalone - let switchExpr = SwitchExprSyntax( - switchKeyword: .keyword(.switch, trailingTrivia: .space), - subject: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("_"))), - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - cases: SwitchCaseListSyntax([SwitchCaseListSyntax.Element(switchCase)]), - rightBrace: .rightBraceToken(leadingTrivia: .newline) - ) - item = .expr(ExprSyntax(switchExpr)) + if let convertedItem = CodeBlockItemSyntax.Item.create(from: self.syntax) { + item = convertedItem } else { fatalError( - "Unsupported syntax type at top level: \(type(of: self.syntax)) (\(self.syntax)) generating from \(self)" + "Unsupported syntax type at top level: \(type(of: self.syntax)) (\(self.syntax)) " + + "generating from \(self)" ) } statements = CodeBlockItemListSyntax([ diff --git a/Sources/SyntaxKit/CodeBlocks/CodeBlockBuilder.swift b/Sources/SyntaxKit/CodeBlocks/CodeBlockBuilder.swift new file mode 100644 index 0000000..74c241f --- /dev/null +++ b/Sources/SyntaxKit/CodeBlocks/CodeBlockBuilder.swift @@ -0,0 +1,38 @@ +// +// CodeBlockBuilder.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 Foundation + +/// A protocol for types that can build a ``CodeBlock``. +public protocol CodeBlockBuilder { + /// The type of ``CodeBlock`` that this builder creates. + associatedtype Result: CodeBlock + /// Builds the ``CodeBlock``. + func build() -> Result +} diff --git a/Sources/SyntaxKit/CodeBlock.swift b/Sources/SyntaxKit/CodeBlocks/CodeBlockBuilderResult.swift similarity index 74% rename from Sources/SyntaxKit/CodeBlock.swift rename to Sources/SyntaxKit/CodeBlocks/CodeBlockBuilderResult.swift index 086e2a1..4340d5c 100644 --- a/Sources/SyntaxKit/CodeBlock.swift +++ b/Sources/SyntaxKit/CodeBlocks/CodeBlockBuilderResult.swift @@ -1,5 +1,5 @@ // -// CodeBlock.swift +// CodeBlockBuilderResult.swift // SyntaxKit // // Created by Leo Dion. @@ -28,21 +28,6 @@ // import Foundation -import SwiftSyntax - -/// A protocol for types that can be represented as a SwiftSyntax node. -public protocol CodeBlock { - /// The SwiftSyntax representation of the code block. - var syntax: SyntaxProtocol { get } -} - -/// A protocol for types that can build a ``CodeBlock``. -public protocol CodeBlockBuilder { - /// The type of ``CodeBlock`` that this builder creates. - associatedtype Result: CodeBlock - /// Builds the ``CodeBlock``. - func build() -> Result -} /// A result builder for creating arrays of ``CodeBlock``s. @resultBuilder @@ -72,11 +57,3 @@ public enum CodeBlockBuilderResult { components } } - -/// An empty ``CodeBlock``. -public struct EmptyCodeBlock: CodeBlock { - /// The syntax for an empty code block. - public var syntax: SyntaxProtocol { - StringSegmentSyntax(content: .unknown("")) - } -} diff --git a/Sources/SyntaxKit/CodeBlocks/CodeBlockItemSyntax.Item.swift b/Sources/SyntaxKit/CodeBlocks/CodeBlockItemSyntax.Item.swift new file mode 100644 index 0000000..17e72fe --- /dev/null +++ b/Sources/SyntaxKit/CodeBlocks/CodeBlockItemSyntax.Item.swift @@ -0,0 +1,62 @@ +// +// CodeBlockItemSyntax.Item.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 + +extension CodeBlockItemSyntax.Item { + /// Creates a `CodeBlockItemSyntax.Item` from a `SyntaxProtocol`. + /// - Parameter syntax: The syntax to convert. + /// - Returns: A `CodeBlockItemSyntax.Item` if the conversion is successful, `nil` otherwise. + public static func create(from syntax: SyntaxProtocol) -> CodeBlockItemSyntax.Item? { + if let decl = syntax.as(DeclSyntax.self) { + return .decl(decl) + } else if let stmt = syntax.as(StmtSyntax.self) { + return .stmt(stmt) + } else if let expr = syntax.as(ExprSyntax.self) { + return .expr(expr) + } else if let token = syntax.as(TokenSyntax.self) { + // Wrap TokenSyntax in DeclReferenceExprSyntax and then in ExprSyntax + let expr = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(token.text))) + return .expr(expr) + } else if let switchCase = syntax.as(SwitchCaseSyntax.self) { + // Wrap SwitchCaseSyntax in a SwitchExprSyntax and treat it as an expression + // This is a fallback for when SwitchCase is used standalone + let switchExpr = SwitchExprSyntax( + switchKeyword: .keyword(.switch, trailingTrivia: .space), + subject: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("_"))), + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + cases: SwitchCaseListSyntax([SwitchCaseListSyntax.Element(switchCase)]), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + return .expr(ExprSyntax(switchExpr)) + } else { + return nil + } + } +} diff --git a/Sources/SyntaxKit/CommentedCodeBlock.swift b/Sources/SyntaxKit/CodeBlocks/CommentedCodeBlock.swift similarity index 97% rename from Sources/SyntaxKit/CommentedCodeBlock.swift rename to Sources/SyntaxKit/CodeBlocks/CommentedCodeBlock.swift index 79f6ef3..606f1c4 100644 --- a/Sources/SyntaxKit/CommentedCodeBlock.swift +++ b/Sources/SyntaxKit/CodeBlocks/CommentedCodeBlock.swift @@ -38,7 +38,9 @@ internal struct CommentedCodeBlock: CodeBlock { internal var syntax: SyntaxProtocol { // Shortcut if there are no comment lines - guard !lines.isEmpty else { return base.syntax } + guard !lines.isEmpty else { + return base.syntax + } let commentTrivia = Trivia(pieces: lines.flatMap { [$0.triviaPiece, TriviaPiece.newlines(1)] }) diff --git a/Sources/SyntaxKit/CodeBlocks/EmptyCodeBlock.swift b/Sources/SyntaxKit/CodeBlocks/EmptyCodeBlock.swift new file mode 100644 index 0000000..6db397c --- /dev/null +++ b/Sources/SyntaxKit/CodeBlocks/EmptyCodeBlock.swift @@ -0,0 +1,39 @@ +// +// EmptyCodeBlock.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 Foundation +import SwiftSyntax + +/// An empty ``CodeBlock``. +public struct EmptyCodeBlock: CodeBlock { + /// The syntax for an empty code block. + public var syntax: SyntaxProtocol { + StringSegmentSyntax(content: .unknown("")) + } +} diff --git a/Sources/SyntaxKit/ExprCodeBlock.swift b/Sources/SyntaxKit/CodeBlocks/ExprCodeBlock.swift similarity index 100% rename from Sources/SyntaxKit/ExprCodeBlock.swift rename to Sources/SyntaxKit/CodeBlocks/ExprCodeBlock.swift diff --git a/Sources/SyntaxKit/Array+LiteralValue.swift b/Sources/SyntaxKit/Collections/Array+LiteralValue.swift similarity index 98% rename from Sources/SyntaxKit/Array+LiteralValue.swift rename to Sources/SyntaxKit/Collections/Array+LiteralValue.swift index baf3c7b..9b75416 100644 --- a/Sources/SyntaxKit/Array+LiteralValue.swift +++ b/Sources/SyntaxKit/Collections/Array+LiteralValue.swift @@ -45,7 +45,8 @@ extension Array: LiteralValue where Element == String { .replacingOccurrences(of: "\r", with: "\\r") .replacingOccurrences(of: "\t", with: "\\t") return "\"\(escaped)\"" - }.joined(separator: ", ") + } + .joined(separator: ", ") return "[\(elements)]" } } diff --git a/Sources/SyntaxKit/ArrayLiteral.swift b/Sources/SyntaxKit/Collections/ArrayLiteral.swift similarity index 96% rename from Sources/SyntaxKit/ArrayLiteral.swift rename to Sources/SyntaxKit/Collections/ArrayLiteral.swift index e752bc1..4117858 100644 --- a/Sources/SyntaxKit/ArrayLiteral.swift +++ b/Sources/SyntaxKit/Collections/ArrayLiteral.swift @@ -29,9 +29,9 @@ import Foundation -/// An array value that can be used as a literal. +/// An array literal value that can be used as a literal. public struct ArrayLiteral: LiteralValue { - let elements: [Literal] + public let elements: [Literal] /// Creates an array with the given elements. /// - Parameter elements: The array elements. diff --git a/Sources/SyntaxKit/Dictionary+LiteralValue.swift b/Sources/SyntaxKit/Collections/Dictionary+LiteralValue.swift similarity index 98% rename from Sources/SyntaxKit/Dictionary+LiteralValue.swift rename to Sources/SyntaxKit/Collections/Dictionary+LiteralValue.swift index 980dc16..183492f 100644 --- a/Sources/SyntaxKit/Dictionary+LiteralValue.swift +++ b/Sources/SyntaxKit/Collections/Dictionary+LiteralValue.swift @@ -45,7 +45,8 @@ extension Dictionary: LiteralValue where Key == Int, Value == String { .replacingOccurrences(of: "\r", with: "\\r") .replacingOccurrences(of: "\t", with: "\\t") return "\(key): \"\(escaped)\"" - }.joined(separator: ", ") + } + .joined(separator: ", ") return "[\(elements)]" } } diff --git a/Sources/SyntaxKit/DictionaryExpr.swift b/Sources/SyntaxKit/Collections/DictionaryExpr.swift similarity index 90% rename from Sources/SyntaxKit/DictionaryExpr.swift rename to Sources/SyntaxKit/Collections/DictionaryExpr.swift index 5ff079c..0006f55 100644 --- a/Sources/SyntaxKit/DictionaryExpr.swift +++ b/Sources/SyntaxKit/Collections/DictionaryExpr.swift @@ -76,14 +76,18 @@ public struct DictionaryExpr: CodeBlock, LiteralValue { elements.enumerated().map { index, keyValue in let (key, value) = keyValue return DictionaryElementSyntax( - keyExpression: key.exprSyntax, + key: key.exprSyntax, colon: .colonToken(), - valueExpression: value.exprSyntax, - trailingComma: index < elements.count - 1 ? .commaToken(trailingTrivia: .space) : nil + value: value.exprSyntax, + trailingComma: index < elements.count - 1 ? .commaToken(trailingTrivia: .newline) : nil ) } ) - return DictionaryExprSyntax(content: .elements(dictionaryElements)) + return DictionaryExprSyntax( + leftSquare: .leftSquareToken(trailingTrivia: .newline), + content: .elements(dictionaryElements), + rightSquare: .rightSquareToken(leadingTrivia: .newline) + ) } } } diff --git a/Sources/SyntaxKit/DictionaryLiteral.swift b/Sources/SyntaxKit/Collections/DictionaryLiteral.swift similarity index 98% rename from Sources/SyntaxKit/DictionaryLiteral.swift rename to Sources/SyntaxKit/Collections/DictionaryLiteral.swift index c2d7de7..03dc8a8 100644 --- a/Sources/SyntaxKit/DictionaryLiteral.swift +++ b/Sources/SyntaxKit/Collections/DictionaryLiteral.swift @@ -31,7 +31,7 @@ import Foundation /// A dictionary literal value that can be used as a literal. public struct DictionaryLiteral: LiteralValue { - let elements: [(Literal, Literal)] + public let elements: [(Literal, Literal)] /// Creates a dictionary with the given key-value pairs. /// - Parameter elements: The dictionary key-value pairs. diff --git a/Sources/SyntaxKit/DictionaryValue.swift b/Sources/SyntaxKit/Collections/DictionaryValue.swift similarity index 100% rename from Sources/SyntaxKit/DictionaryValue.swift rename to Sources/SyntaxKit/Collections/DictionaryValue.swift diff --git a/Sources/SyntaxKit/Tuple.swift b/Sources/SyntaxKit/Collections/Tuple.swift similarity index 100% rename from Sources/SyntaxKit/Tuple.swift rename to Sources/SyntaxKit/Collections/Tuple.swift diff --git a/Sources/SyntaxKit/TupleAssignment+AsyncSet.swift b/Sources/SyntaxKit/Collections/TupleAssignment+AsyncSet.swift similarity index 92% rename from Sources/SyntaxKit/TupleAssignment+AsyncSet.swift rename to Sources/SyntaxKit/Collections/TupleAssignment+AsyncSet.swift index f28bd41..2606a41 100644 --- a/Sources/SyntaxKit/TupleAssignment+AsyncSet.swift +++ b/Sources/SyntaxKit/Collections/TupleAssignment+AsyncSet.swift @@ -31,7 +31,7 @@ import SwiftSyntax extension TupleAssignment { internal enum AsyncSet { - static func tuplePattern(elements: [String]) -> PatternSyntax { + internal static func tuplePattern(elements: [String]) -> PatternSyntax { let patternElements = TuplePatternElementListSyntax( elements.enumerated().map { index, element in TuplePatternElementSyntax( @@ -51,7 +51,7 @@ extension TupleAssignment { ) } - static func tupleExpr(tuple: Tuple) -> ExprSyntax { + internal static func tupleExpr(tuple: Tuple) -> ExprSyntax { ExprSyntax( TupleExprSyntax( leftParen: .leftParenToken(), @@ -71,7 +71,7 @@ extension TupleAssignment { ) } - static func valueExpr(tupleExpr: ExprSyntax, isThrowing: Bool) -> ExprSyntax { + internal static func valueExpr(tupleExpr: ExprSyntax, isThrowing: Bool) -> ExprSyntax { isThrowing ? ExprSyntax( TryExprSyntax( diff --git a/Sources/SyntaxKit/TupleAssignment.swift b/Sources/SyntaxKit/Collections/TupleAssignment.swift similarity index 98% rename from Sources/SyntaxKit/TupleAssignment.swift rename to Sources/SyntaxKit/Collections/TupleAssignment.swift index 10da5e6..7a96427 100644 --- a/Sources/SyntaxKit/TupleAssignment.swift +++ b/Sources/SyntaxKit/Collections/TupleAssignment.swift @@ -84,7 +84,8 @@ public struct TupleAssignment: CodeBlock { // Generate a single async let tuple destructuring assignment guard let tuple = value as? Tuple, elements.count == tuple.elements.count else { fatalError( - "asyncSet requires a Tuple value with the same number of elements as the assignment.") + "asyncSet requires a Tuple value with the same number of elements as the assignment." + ) } // Use helpers from AsyncSet @@ -168,7 +169,9 @@ public struct TupleAssignment: CodeBlock { private func buildValueExpression() -> ExprSyntax { let baseExpr = value.syntax.as(ExprSyntax.self) - ?? ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(""))) + ?? ExprSyntax( + DeclReferenceExprSyntax(baseName: .identifier("")) + ) if isThrowing { if isAsync { diff --git a/Sources/SyntaxKit/TupleLiteral.swift b/Sources/SyntaxKit/Collections/TupleLiteral.swift similarity index 98% rename from Sources/SyntaxKit/TupleLiteral.swift rename to Sources/SyntaxKit/Collections/TupleLiteral.swift index ecf3444..4bbba1a 100644 --- a/Sources/SyntaxKit/TupleLiteral.swift +++ b/Sources/SyntaxKit/Collections/TupleLiteral.swift @@ -31,7 +31,7 @@ import Foundation /// A tuple literal value that can be used as a literal. public struct TupleLiteral: LiteralValue { - let elements: [Literal?] + public let elements: [Literal?] /// Creates a tuple with the given elements. /// - Parameter elements: The tuple elements, where `nil` represents a wildcard. diff --git a/Sources/SyntaxKit/TuplePattern.swift b/Sources/SyntaxKit/Collections/TuplePattern.swift similarity index 100% rename from Sources/SyntaxKit/TuplePattern.swift rename to Sources/SyntaxKit/Collections/TuplePattern.swift diff --git a/Sources/SyntaxKit/TuplePatternCodeBlock.swift b/Sources/SyntaxKit/Collections/TuplePatternCodeBlock.swift similarity index 100% rename from Sources/SyntaxKit/TuplePatternCodeBlock.swift rename to Sources/SyntaxKit/Collections/TuplePatternCodeBlock.swift diff --git a/Sources/SyntaxKit/Do.swift b/Sources/SyntaxKit/ControlFlow/Do.swift similarity index 100% rename from Sources/SyntaxKit/Do.swift rename to Sources/SyntaxKit/ControlFlow/Do.swift diff --git a/Sources/SyntaxKit/For.swift b/Sources/SyntaxKit/ControlFlow/For.swift similarity index 99% rename from Sources/SyntaxKit/For.swift rename to Sources/SyntaxKit/ControlFlow/For.swift index ed3c963..ff3a5d1 100644 --- a/Sources/SyntaxKit/For.swift +++ b/Sources/SyntaxKit/ControlFlow/For.swift @@ -118,7 +118,8 @@ public struct For: CodeBlock { item = CodeBlockItemSyntax(item: .stmt(stmt)) } return item?.with(\.trailingTrivia, .newline) - }), + } + ), rightBrace: .rightBraceToken(leadingTrivia: .newline) ) diff --git a/Sources/SyntaxKit/Guard.swift b/Sources/SyntaxKit/ControlFlow/Guard.swift similarity index 100% rename from Sources/SyntaxKit/Guard.swift rename to Sources/SyntaxKit/ControlFlow/Guard.swift diff --git a/Sources/SyntaxKit/ControlFlow/If+Body.swift b/Sources/SyntaxKit/ControlFlow/If+Body.swift new file mode 100644 index 0000000..91d6525 --- /dev/null +++ b/Sources/SyntaxKit/ControlFlow/If+Body.swift @@ -0,0 +1,50 @@ +// +// If+Body.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 + +extension If { + /// Builds the body block for the if statement. + internal func buildBody() -> CodeBlockSyntax { + CodeBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + statements: buildBodyStatements(from: body), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + } + + /// Builds the statements for a code block from an array of CodeBlocks. + internal func buildBodyStatements(from blocks: [CodeBlock]) -> CodeBlockItemListSyntax { + CodeBlockItemListSyntax( + blocks.compactMap { block in + createCodeBlockItem(from: block)?.with(\.trailingTrivia, .newline) + } + ) + } +} diff --git a/Sources/SyntaxKit/ControlFlow/If+CodeBlockItem.swift b/Sources/SyntaxKit/ControlFlow/If+CodeBlockItem.swift new file mode 100644 index 0000000..c7c41af --- /dev/null +++ b/Sources/SyntaxKit/ControlFlow/If+CodeBlockItem.swift @@ -0,0 +1,47 @@ +// +// If+CodeBlockItem.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 + +extension If { + /// Creates a code block item from a CodeBlock. + internal func createCodeBlockItem(from block: CodeBlock) -> CodeBlockItemSyntax? { + if let enumCase = block as? EnumCase { + // Handle EnumCase specially - use expression syntax for enum cases in expressions + return CodeBlockItemSyntax(item: .expr(enumCase.exprSyntax)) + } else if let decl = block.syntax.as(DeclSyntax.self) { + return CodeBlockItemSyntax(item: .decl(decl)) + } else if let expr = block.syntax.as(ExprSyntax.self) { + return CodeBlockItemSyntax(item: .expr(expr)) + } else if let stmt = block.syntax.as(StmtSyntax.self) { + return CodeBlockItemSyntax(item: .stmt(stmt)) + } + return nil + } +} diff --git a/Sources/SyntaxKit/ControlFlow/If+Conditions.swift b/Sources/SyntaxKit/ControlFlow/If+Conditions.swift new file mode 100644 index 0000000..61b7725 --- /dev/null +++ b/Sources/SyntaxKit/ControlFlow/If+Conditions.swift @@ -0,0 +1,92 @@ +// +// If+Conditions.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 + +extension If { + /// Builds the condition elements for the if statement. + internal func buildConditions() -> ConditionElementListSyntax { + ConditionElementListSyntax( + conditions.enumerated().map { index, block in + let needsComma = index < conditions.count - 1 + return buildConditionElement(from: block, needsComma: needsComma) + } + ) + } + + /// Builds a single condition element from a code block. + private func buildConditionElement( + from block: CodeBlock, + needsComma: Bool + ) -> ConditionElementSyntax { + let element = createConditionElement(from: block) + return appendCommaIfNeeded(element, needsComma: needsComma) + } + + /// Creates a condition element from a code block. + private func createConditionElement(from block: CodeBlock) -> ConditionElementSyntax { + if let letCond = block as? Let { + return ConditionElementSyntax( + condition: .optionalBinding( + OptionalBindingConditionSyntax( + bindingSpecifier: .keyword(.let, trailingTrivia: .space), + pattern: IdentifierPatternSyntax( + identifier: .identifier(letCond.name) + ), + initializer: InitializerClauseSyntax( + equal: .equalToken( + leadingTrivia: .space, + trailingTrivia: .space + ), + value: letCond.value.syntax.as(ExprSyntax.self) + ?? ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(""))) + ) + ) + ) + ) + } else { + return ConditionElementSyntax( + condition: .expression( + ExprSyntax( + fromProtocol: block.syntax.as(ExprSyntax.self) + ?? DeclReferenceExprSyntax(baseName: .identifier("")) + ) + ) + ) + } + } + + /// Appends a comma to the condition element if needed. + private func appendCommaIfNeeded( + _ element: ConditionElementSyntax, + needsComma: Bool + ) -> ConditionElementSyntax { + needsComma ? element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) : element + } +} diff --git a/Sources/SyntaxKit/ControlFlow/If+ElseBody.swift b/Sources/SyntaxKit/ControlFlow/If+ElseBody.swift new file mode 100644 index 0000000..926b490 --- /dev/null +++ b/Sources/SyntaxKit/ControlFlow/If+ElseBody.swift @@ -0,0 +1,144 @@ +// +// If+ElseBody.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 + +extension If { + /// Builds the else body for the if statement, handling else-if chains. + internal func buildElseBody() -> IfExprSyntax.ElseBody? { + guard let elseBlocks = elseBody else { + return nil + } + + // Build a chained else-if structure if the builder provided If blocks. + var current: SyntaxProtocol? + + for block in elseBlocks.reversed() { + current = processElseBlock(block, current: current) + } + + return createElseBody(from: current) + } + + /// Processes a single else block and updates the current syntax. + private func processElseBlock( + _ block: CodeBlock, + current: SyntaxProtocol? + ) -> SyntaxProtocol? { + switch block { + case let thenBlock as Then: + return buildThenBlock(thenBlock) + case let ifBlock as If: + return processIfBlock(ifBlock, current: current) + default: + return buildDefaultElseBlock(block) + } + } + + /// Builds a Then block for the else clause. + private func buildThenBlock(_ thenBlock: Then) -> SyntaxProtocol { + let stmts = CodeBlockItemListSyntax( + thenBlock.body.compactMap { element in + createCodeBlockItem(from: element)?.with(\.trailingTrivia, .newline) + } + ) + return CodeBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + statements: stmts, + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) as SyntaxProtocol + } + + /// Creates an else choice from a syntax protocol. + private func createElseChoice(from nested: SyntaxProtocol) -> IfExprSyntax.ElseBody { + if let codeBlock = nested.as(CodeBlockSyntax.self) { + return IfExprSyntax.ElseBody(codeBlock) + } else if let nestedIf = nested.as(IfExprSyntax.self) { + return IfExprSyntax.ElseBody(nestedIf) + } else { + // Fallback to empty code block + return IfExprSyntax.ElseBody( + CodeBlockSyntax( + leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), + statements: CodeBlockItemListSyntax([]), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + ) + } + } + + /// Processes an If block to build the else-if chain. + private func processIfBlock( + _ ifBlock: If, + current: SyntaxProtocol? + ) -> SyntaxProtocol? { + guard var ifExpr = ifBlock.syntax.as(IfExprSyntax.self) else { + return current + } + + if let nested = current { + let elseChoice = createElseChoice(from: nested) + ifExpr = + ifExpr + .with(\.elseKeyword, .keyword(.else, leadingTrivia: .space, trailingTrivia: .space)) + .with(\.elseBody, elseChoice) + } + return ifExpr as SyntaxProtocol + } + + /// Builds a default else block for any other CodeBlock type. + private func buildDefaultElseBlock(_ block: CodeBlock) -> SyntaxProtocol? { + guard let item = createCodeBlockItem(from: block) else { + return nil + } + + return CodeBlockSyntax( + leftBrace: .leftBraceToken( + leadingTrivia: .space, + trailingTrivia: .newline + ), + statements: CodeBlockItemListSyntax([item.with(\.trailingTrivia, .newline)]), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + } + + /// Creates the final else body from the processed syntax. + private func createElseBody(from current: SyntaxProtocol?) -> IfExprSyntax.ElseBody? { + guard let final = current else { + return nil + } + + if let codeBlock = final.as(CodeBlockSyntax.self) { + return IfExprSyntax.ElseBody(codeBlock) + } else if let ifExpr = final.as(IfExprSyntax.self) { + return IfExprSyntax.ElseBody(ifExpr) + } + return nil + } +} diff --git a/Sources/SyntaxKit/ControlFlow/If.swift b/Sources/SyntaxKit/ControlFlow/If.swift new file mode 100644 index 0000000..65bcca7 --- /dev/null +++ b/Sources/SyntaxKit/ControlFlow/If.swift @@ -0,0 +1,86 @@ +// +// If.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 + +/// An `if` statement. +public struct If: CodeBlock { + internal let conditions: [CodeBlock] + internal let body: [CodeBlock] + internal let elseBody: [CodeBlock]? + + /// Creates an `if` statement with optional `else`. + /// - Parameters: + /// - condition: A single `CodeBlock` produced by the builder that describes the `if` condition. + /// - then: Builder that produces the body for the `if` branch. + /// - elseBody: Builder that produces the body for the `else` branch. The body may contain + /// nested `If` instances (representing `else if`) and/or a ``Then`` block for the + /// final `else` statements. + public init( + @CodeBlockBuilderResult _ condition: () -> [CodeBlock], + @CodeBlockBuilderResult then: () -> [CodeBlock], + @CodeBlockBuilderResult else elseBody: () -> [CodeBlock] = { [] } + ) { + let allConditions = condition() + guard !allConditions.isEmpty else { + fatalError("If requires at least one condition CodeBlock") + } + self.conditions = allConditions + self.body = then() + let generatedElse = elseBody() + self.elseBody = generatedElse.isEmpty ? nil : generatedElse + } + + /// Convenience initializer that keeps the previous API: pass the condition directly. + public init( + _ condition: CodeBlock, + @CodeBlockBuilderResult then: () -> [CodeBlock], + @CodeBlockBuilderResult else elseBody: () -> [CodeBlock] = { [] } + ) { + self.init({ condition }, then: then, else: elseBody) + } + + public var syntax: SyntaxProtocol { + // Build list of ConditionElements from all provided conditions + let condList = buildConditions() + let bodyBlock = buildBody() + let elseBlock = buildElseBody() + + return ExprSyntax( + IfExprSyntax( + ifKeyword: .keyword(.if, trailingTrivia: .space), + conditions: condList, + body: bodyBlock, + elseKeyword: elseBlock != nil + ? .keyword(.else, leadingTrivia: .space, trailingTrivia: .space) : nil, + elseBody: elseBlock + ) + ) + } +} diff --git a/Sources/SyntaxKit/Switch.swift b/Sources/SyntaxKit/ControlFlow/Switch.swift similarity index 90% rename from Sources/SyntaxKit/Switch.swift rename to Sources/SyntaxKit/ControlFlow/Switch.swift index bd44369..1def183 100644 --- a/Sources/SyntaxKit/Switch.swift +++ b/Sources/SyntaxKit/ControlFlow/Switch.swift @@ -58,9 +58,15 @@ public struct Switch: CodeBlock { ?? DeclReferenceExprSyntax(baseName: .identifier("")) ) let casesArr: [SwitchCaseSyntax] = self.cases.compactMap { - if let tupleCase = $0 as? Case { return tupleCase.switchCaseSyntax } - if let switchCase = $0 as? SwitchCase { return switchCase.switchCaseSyntax } - if let switchDefault = $0 as? Default { return switchDefault.switchCaseSyntax } + if let tupleCase = $0 as? Case { + return tupleCase.switchCaseSyntax + } + if let switchCase = $0 as? SwitchCase { + return switchCase.switchCaseSyntax + } + if let switchDefault = $0 as? Default { + return switchDefault.switchCaseSyntax + } return nil } let cases = SwitchCaseListSyntax(casesArr.map { SwitchCaseListSyntax.Element($0) }) diff --git a/Sources/SyntaxKit/SwitchCase.swift b/Sources/SyntaxKit/ControlFlow/SwitchCase.swift similarity index 95% rename from Sources/SyntaxKit/SwitchCase.swift rename to Sources/SyntaxKit/ControlFlow/SwitchCase.swift index 0b579cc..5e10279 100644 --- a/Sources/SyntaxKit/SwitchCase.swift +++ b/Sources/SyntaxKit/ControlFlow/SwitchCase.swift @@ -66,7 +66,8 @@ public struct SwitchCase: CodeBlock { } else if let variableExp = pattern as? VariableExp { // Handle VariableExp specially - convert to identifier pattern patternSyntax = PatternSyntax( - IdentifierPatternSyntax(identifier: .identifier(variableExp.name))) + IdentifierPatternSyntax(identifier: .identifier(variableExp.name)) + ) } else if let codeBlock = pattern as? CodeBlock { // Convert CodeBlock to expression pattern let expr = ExprSyntax( @@ -80,10 +81,14 @@ public struct SwitchCase: CodeBlock { var item = SwitchCaseItemSyntax(pattern: patternSyntax) if index < patterns.count - 1 { - item = item.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + item = item.with( + \.trailingComma, + .commaToken(trailingTrivia: .space) + ) } return item - }) + } + ) // Handle special case for multiple conditionals with let binding and where clause var finalCaseItems = caseItems @@ -104,7 +109,11 @@ public struct SwitchCase: CodeBlock { ) let whereClause = WhereClauseSyntax( - whereKeyword: .keyword(.where, leadingTrivia: .space, trailingTrivia: .space), + whereKeyword: .keyword( + .where, + leadingTrivia: .space, + trailingTrivia: .space + ), condition: whereExpr ) diff --git a/Sources/SyntaxKit/SwitchLet.swift b/Sources/SyntaxKit/ControlFlow/SwitchLet.swift similarity index 94% rename from Sources/SyntaxKit/SwitchLet.swift rename to Sources/SyntaxKit/ControlFlow/SwitchLet.swift index 617fa22..ec3153a 100644 --- a/Sources/SyntaxKit/SwitchLet.swift +++ b/Sources/SyntaxKit/ControlFlow/SwitchLet.swift @@ -40,12 +40,15 @@ public struct SwitchLet: PatternConvertible, CodeBlock { } public var patternSyntax: PatternSyntax { - let identifier = IdentifierPatternSyntax(identifier: .identifier(name)) + let identifier = IdentifierPatternSyntax( + identifier: .identifier(name) + ) return PatternSyntax( ValueBindingPatternSyntax( bindingSpecifier: .keyword(.let, trailingTrivia: .space), pattern: identifier - )) + ) + ) } public var syntax: SyntaxProtocol { diff --git a/Sources/SyntaxKit/While.swift b/Sources/SyntaxKit/ControlFlow/While.swift similarity index 94% rename from Sources/SyntaxKit/While.swift rename to Sources/SyntaxKit/ControlFlow/While.swift index 1d2aafc..44080be 100644 --- a/Sources/SyntaxKit/While.swift +++ b/Sources/SyntaxKit/ControlFlow/While.swift @@ -80,18 +80,21 @@ public struct While: CodeBlock { item = CodeBlockItemSyntax(item: .stmt(stmt)) } return item?.with(\.trailingTrivia, .newline) - }), + } + ), rightBrace: .rightBraceToken(leadingTrivia: .newline) ) return StmtSyntax( WhileStmtSyntax( whileKeyword: .keyword(.while, trailingTrivia: .space), - conditions: ConditionElementListSyntax([ - ConditionElementSyntax( - condition: .expression(conditionExpr) - ) - ]), + conditions: ConditionElementListSyntax( + [ + ConditionElementSyntax( + condition: .expression(conditionExpr) + ) + ] + ), body: bodyBlock ) ) diff --git a/Sources/SyntaxKit/Core/CodeBlock.swift b/Sources/SyntaxKit/Core/CodeBlock.swift new file mode 100644 index 0000000..e6d5588 --- /dev/null +++ b/Sources/SyntaxKit/Core/CodeBlock.swift @@ -0,0 +1,37 @@ +// +// CodeBlock.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 Foundation +import SwiftSyntax + +/// A protocol for types that can be represented as a SwiftSyntax node. +public protocol CodeBlock { + /// The SwiftSyntax representation of the code block. + var syntax: SyntaxProtocol { get } +} diff --git a/Sources/SyntaxKit/Line.swift b/Sources/SyntaxKit/Core/Line.swift similarity index 73% rename from Sources/SyntaxKit/Line.swift rename to Sources/SyntaxKit/Core/Line.swift index 5127453..3017ae6 100644 --- a/Sources/SyntaxKit/Line.swift +++ b/Sources/SyntaxKit/Core/Line.swift @@ -27,27 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -/// 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 /// Represents a single comment line that can be attached to a syntax node. diff --git a/Sources/SyntaxKit/Core/PatternConvertible.swift b/Sources/SyntaxKit/Core/PatternConvertible.swift new file mode 100644 index 0000000..de8d77e --- /dev/null +++ b/Sources/SyntaxKit/Core/PatternConvertible.swift @@ -0,0 +1,37 @@ +// +// PatternConvertible.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 Foundation +import SwiftSyntax + +/// Types that can be turned into a `PatternSyntax` suitable for a `switch` case pattern. +public protocol PatternConvertible { + /// SwiftSyntax representation of the pattern. + var patternSyntax: PatternSyntax { get } +} diff --git a/Sources/SyntaxKit/Class.swift b/Sources/SyntaxKit/Declarations/Class.swift similarity index 99% rename from Sources/SyntaxKit/Class.swift rename to Sources/SyntaxKit/Declarations/Class.swift index bf28ff4..56446f3 100644 --- a/Sources/SyntaxKit/Class.swift +++ b/Sources/SyntaxKit/Declarations/Class.swift @@ -141,7 +141,9 @@ public struct Class: CodeBlock { leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), members: MemberBlockItemListSyntax( members.compactMap { member in - guard let decl = member.syntax.as(DeclSyntax.self) else { return nil } + guard let decl = member.syntax.as(DeclSyntax.self) else { + return nil + } return MemberBlockItemSyntax(decl: decl, trailingTrivia: .newline) } ), diff --git a/Sources/SyntaxKit/Enum.swift b/Sources/SyntaxKit/Declarations/Enum.swift similarity index 97% rename from Sources/SyntaxKit/Enum.swift rename to Sources/SyntaxKit/Declarations/Enum.swift index a8c8e52..1945142 100644 --- a/Sources/SyntaxKit/Enum.swift +++ b/Sources/SyntaxKit/Declarations/Enum.swift @@ -73,7 +73,8 @@ public struct Enum: CodeBlock { if !inheritance.isEmpty { let inheritedTypes = inheritance.map { type in InheritedTypeSyntax( - type: IdentifierTypeSyntax(name: .identifier(type))) + type: IdentifierTypeSyntax(name: .identifier(type)) + ) } inheritanceClause = InheritanceClauseSyntax( colon: .colonToken(), @@ -96,9 +97,12 @@ public struct Enum: CodeBlock { leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), members: MemberBlockItemListSyntax( members.compactMap { member in - guard let syntax = member.syntax.as(DeclSyntax.self) else { return nil } + guard let syntax = member.syntax.as(DeclSyntax.self) else { + return nil + } return MemberBlockItemSyntax(decl: syntax, trailingTrivia: .newline) - }), + } + ), rightBrace: .rightBraceToken(leadingTrivia: .newline) ) diff --git a/Sources/SyntaxKit/Extension.swift b/Sources/SyntaxKit/Declarations/Extension.swift similarity index 98% rename from Sources/SyntaxKit/Extension.swift rename to Sources/SyntaxKit/Declarations/Extension.swift index a61f803..af53db4 100644 --- a/Sources/SyntaxKit/Extension.swift +++ b/Sources/SyntaxKit/Declarations/Extension.swift @@ -92,9 +92,12 @@ public struct Extension: CodeBlock { leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), members: MemberBlockItemListSyntax( members.compactMap { member in - guard let syntax = member.syntax.as(DeclSyntax.self) else { return nil } + guard let syntax = member.syntax.as(DeclSyntax.self) else { + return nil + } return MemberBlockItemSyntax(decl: syntax, trailingTrivia: .newline) - }), + } + ), rightBrace: .rightBraceToken(leadingTrivia: .newline) ) diff --git a/Sources/SyntaxKit/Init.swift b/Sources/SyntaxKit/Declarations/Init.swift similarity index 90% rename from Sources/SyntaxKit/Init.swift rename to Sources/SyntaxKit/Declarations/Init.swift index acf8122..5ef1c2c 100644 --- a/Sources/SyntaxKit/Init.swift +++ b/Sources/SyntaxKit/Declarations/Init.swift @@ -57,17 +57,24 @@ public struct Init: CodeBlock, ExprCodeBlock, LiteralValue { return nil } if index < parameters.count - 1 { - return element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + return element.with( + \.trailingComma, + .commaToken(trailingTrivia: .space) + ) } return element - }) + } + ) return ExprSyntax( FunctionCallExprSyntax( - calledExpression: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(type))), + calledExpression: ExprSyntax( + DeclReferenceExprSyntax(baseName: .identifier(type)) + ), leftParen: .leftParenToken(), arguments: args, rightParen: .rightParenToken() - )) + ) + ) } public var syntax: SyntaxProtocol { diff --git a/Sources/SyntaxKit/Protocol.swift b/Sources/SyntaxKit/Declarations/Protocol.swift similarity index 99% rename from Sources/SyntaxKit/Protocol.swift rename to Sources/SyntaxKit/Declarations/Protocol.swift index bc11002..018af73 100644 --- a/Sources/SyntaxKit/Protocol.swift +++ b/Sources/SyntaxKit/Declarations/Protocol.swift @@ -97,7 +97,9 @@ public struct Protocol: CodeBlock { leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), members: MemberBlockItemListSyntax( members.compactMap { member in - guard let decl = member.syntax.as(DeclSyntax.self) else { return nil } + guard let decl = member.syntax.as(DeclSyntax.self) else { + return nil + } return MemberBlockItemSyntax(decl: decl, trailingTrivia: .newline) } ), diff --git a/Sources/SyntaxKit/Struct.swift b/Sources/SyntaxKit/Declarations/Struct.swift similarity index 97% rename from Sources/SyntaxKit/Struct.swift rename to Sources/SyntaxKit/Declarations/Struct.swift index cbf7b76..e4dc406 100644 --- a/Sources/SyntaxKit/Struct.swift +++ b/Sources/SyntaxKit/Declarations/Struct.swift @@ -96,7 +96,8 @@ public struct Struct: CodeBlock { if !inheritance.isEmpty { let inheritedTypes = inheritance.map { type in InheritedTypeSyntax( - type: IdentifierTypeSyntax(name: .identifier(type))) + type: IdentifierTypeSyntax(name: .identifier(type)) + ) } inheritanceClause = InheritanceClauseSyntax( colon: .colonToken(), @@ -119,9 +120,12 @@ public struct Struct: CodeBlock { leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), members: MemberBlockItemListSyntax( members.compactMap { member in - guard let syntax = member.syntax.as(DeclSyntax.self) else { return nil } + guard let syntax = member.syntax.as(DeclSyntax.self) else { + return nil + } return MemberBlockItemSyntax(decl: syntax, trailingTrivia: .newline) - }), + } + ), rightBrace: .rightBraceToken(leadingTrivia: .newline) ) diff --git a/Sources/SyntaxKit/TypeAlias.swift b/Sources/SyntaxKit/Declarations/TypeAlias.swift similarity index 100% rename from Sources/SyntaxKit/TypeAlias.swift rename to Sources/SyntaxKit/Declarations/TypeAlias.swift diff --git a/Sources/SyntaxKit/Catch.swift b/Sources/SyntaxKit/ErrorHandling/Catch.swift similarity index 99% rename from Sources/SyntaxKit/Catch.swift rename to Sources/SyntaxKit/ErrorHandling/Catch.swift index a4db19a..edad97f 100644 --- a/Sources/SyntaxKit/Catch.swift +++ b/Sources/SyntaxKit/ErrorHandling/Catch.swift @@ -181,7 +181,8 @@ public struct Catch: CodeBlock { item = CodeBlockItemSyntax(item: .stmt(stmt)) } return item?.with(\.trailingTrivia, .newline) - }), + } + ), rightBrace: .rightBraceToken(leadingTrivia: .newline, trailingTrivia: .space) ) diff --git a/Sources/SyntaxKit/ErrorHandling/CatchBuilder.swift b/Sources/SyntaxKit/ErrorHandling/CatchBuilder.swift new file mode 100644 index 0000000..0e05237 --- /dev/null +++ b/Sources/SyntaxKit/ErrorHandling/CatchBuilder.swift @@ -0,0 +1,157 @@ +// +// CatchBuilder.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 + +/// A result builder for creating catch clauses in a do-catch statement. +/// +/// `CatchBuilder` enables the creation of multiple catch clauses using Swift's result builder syntax. +/// It's used in conjunction with the ``Do`` struct to build comprehensive error handling blocks. +/// +/// ## Overview +/// +/// The `CatchBuilder` implements the result builder pattern to allow for declarative construction +/// of catch clauses. It supports various catch clause types including: +/// - Pattern-based catch clauses (e.g., `catch .specificError`) +/// - Catch clauses with associated values (e.g., `catch .error(let code, let message)`) +/// - General catch clauses (e.g., `catch`) +/// +/// ## Usage +/// +/// ```swift +/// Do { +/// // Code that may throw +/// try someFunction() +/// } catch: { +/// Catch(EnumCase("networkError")) { +/// Call("handleNetworkError")() +/// } +/// Catch(EnumCase("validationError").associatedValue("field", type: "String")) { +/// Call("handleValidationError") { +/// ParameterExp(name: "field", value: VariableExp("field")) +/// } +/// } +/// Catch { +/// Call("handleGenericError")() +/// } +/// } +/// ``` +/// +/// ## Result Builder Methods +/// +/// The builder provides several methods that implement the result builder protocol: +/// - `buildBlock`: Combines multiple catch clauses into a list +/// - `buildExpression`: Converts individual catch expressions +/// - `buildOptional`: Handles optional catch clauses +/// - `buildEither`: Supports conditional catch clauses +/// - `buildArray`: Handles array-based catch clause construction +/// +/// ## Integration with SwiftSyntax +/// +/// The builder produces `CatchClauseListSyntax` which is directly compatible with SwiftSyntax's +/// `DoStmtSyntax`, enabling seamless integration with the Swift compiler's syntax tree. +@resultBuilder +public enum CatchBuilder { + /// Combines multiple catch clauses into a `CatchClauseListSyntax`. + /// + /// This method is called by the result builder when multiple catch clauses are provided + /// in the catch block. It creates a list of catch clauses that can be attached to a do statement. + /// + /// - Parameter components: The catch clauses to combine. + /// - Returns: A `CatchClauseListSyntax` containing all the provided catch clauses. + public static func buildBlock(_ components: CatchClauseSyntax...) -> CatchClauseListSyntax { + CatchClauseListSyntax(components) + } + + /// Converts a `CatchClauseSyntax` expression into a catch clause. + /// + /// This method handles direct `CatchClauseSyntax` instances, allowing for custom + /// catch clause construction when needed. + /// + /// - Parameter expression: The catch clause syntax to convert. + /// - Returns: The same catch clause syntax. + public static func buildExpression(_ expression: CatchClauseSyntax) -> CatchClauseSyntax { + expression + } + + /// Converts a ``Catch`` instance into a `CatchClauseSyntax`. + /// + /// This method handles ``Catch`` struct instances, converting them into the appropriate + /// syntax representation. This is the most common use case when working with the DSL. + /// + /// - Parameter expression: The `Catch` instance to convert. + /// - Returns: A `CatchClauseSyntax` representing the catch clause. + public static func buildExpression(_ expression: Catch) -> CatchClauseSyntax { + expression.catchClauseSyntax + } + + /// Handles optional catch clauses in conditional contexts. + /// + /// This method supports conditional catch clause construction, allowing for + /// catch clauses that may or may not be included based on runtime conditions. + /// + /// - Parameter component: An optional catch clause. + /// - Returns: The optional catch clause, unchanged. + public static func buildOptional(_ component: CatchClauseSyntax?) -> CatchClauseSyntax? { + component + } + + /// Handles the first branch of a conditional catch clause. + /// + /// This method supports `if-else` constructs within catch blocks, allowing for + /// conditional error handling logic. + /// + /// - Parameter component: The catch clause for the first branch. + /// - Returns: The catch clause syntax. + public static func buildEither(first component: CatchClauseSyntax) -> CatchClauseSyntax { + component + } + + /// Handles the second branch of a conditional catch clause. + /// + /// This method supports `if-else` constructs within catch blocks, allowing for + /// conditional error handling logic. + /// + /// - Parameter component: The catch clause for the second branch. + /// - Returns: The catch clause syntax. + public static func buildEither(second component: CatchClauseSyntax) -> CatchClauseSyntax { + component + } + + /// Combines an array of catch clauses into a `CatchClauseListSyntax`. + /// + /// This method supports array-based catch clause construction, useful for + /// dynamic catch clause generation or when working with collections of catch patterns. + /// + /// - Parameter components: An array of catch clauses to combine. + /// - Returns: A `CatchClauseListSyntax` containing all the catch clauses. + public static func buildArray(_ components: [CatchClauseSyntax]) -> CatchClauseListSyntax { + CatchClauseListSyntax(components) + } +} diff --git a/Sources/SyntaxKit/Throw.swift b/Sources/SyntaxKit/ErrorHandling/Throw.swift similarity index 100% rename from Sources/SyntaxKit/Throw.swift rename to Sources/SyntaxKit/ErrorHandling/Throw.swift diff --git a/Sources/SyntaxKit/Assignment.swift b/Sources/SyntaxKit/Expressions/Assignment.swift similarity index 100% rename from Sources/SyntaxKit/Assignment.swift rename to Sources/SyntaxKit/Expressions/Assignment.swift diff --git a/Sources/SyntaxKit/Call.swift b/Sources/SyntaxKit/Expressions/Call.swift similarity index 91% rename from Sources/SyntaxKit/Call.swift rename to Sources/SyntaxKit/Expressions/Call.swift index f3df787..a72843f 100644 --- a/Sources/SyntaxKit/Call.swift +++ b/Sources/SyntaxKit/Expressions/Call.swift @@ -76,7 +76,10 @@ public struct Call: CodeBlock { if let labeled = expr as? LabeledExprSyntax { var element = labeled if index < parameters.count - 1 { - element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + element = element.with( + \.trailingComma, + .commaToken(trailingTrivia: .space) + ) } return element } else if let unlabeled = expr as? ExprSyntax { @@ -84,15 +87,20 @@ public struct Call: CodeBlock { label: nil, colon: nil, expression: unlabeled, - trailingComma: index < parameters.count - 1 ? .commaToken(trailingTrivia: .space) : nil + trailingComma: index < parameters.count - 1 + ? .commaToken(trailingTrivia: .space) + : nil ) } else { fatalError("ParameterExp.syntax must return LabeledExprSyntax or ExprSyntax") } - }) + } + ) let functionCall = FunctionCallExprSyntax( - calledExpression: ExprSyntax(DeclReferenceExprSyntax(baseName: function)), + calledExpression: ExprSyntax( + DeclReferenceExprSyntax(baseName: function) + ), leftParen: .leftParenToken(), arguments: args, rightParen: .rightParenToken() diff --git a/Sources/SyntaxKit/Expressions/FunctionCallExp.swift b/Sources/SyntaxKit/Expressions/FunctionCallExp.swift new file mode 100644 index 0000000..e30e5d5 --- /dev/null +++ b/Sources/SyntaxKit/Expressions/FunctionCallExp.swift @@ -0,0 +1,103 @@ +// +// FunctionCallExp.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 + +/// An expression that calls a function. +public struct FunctionCallExp: CodeBlock { + internal let baseName: String + internal let methodName: String + internal let parameters: [ParameterExp] + + /// Creates a function call expression. + /// - Parameters: + /// - baseName: The name of the base variable. + /// - methodName: The name of the method to call. + public init(baseName: String, methodName: String) { + self.baseName = baseName + self.methodName = methodName + self.parameters = [] + } + + /// Creates a function call expression with parameters. + /// - Parameters: + /// - baseName: The name of the base variable. + /// - methodName: The name of the method to call. + /// - parameters: The parameters for the method call. + public init(baseName: String, methodName: String, parameters: [ParameterExp]) { + self.baseName = baseName + self.methodName = methodName + self.parameters = parameters + } + + public var syntax: SyntaxProtocol { + let base = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(baseName))) + let method = TokenSyntax.identifier(methodName) + let args = LabeledExprListSyntax( + parameters.enumerated().map { index, param in + let expr = param.syntax + if let labeled = expr as? LabeledExprSyntax { + var element = labeled + if index < parameters.count - 1 { + element = element.with( + \.trailingComma, + .commaToken(trailingTrivia: .space) + ) + } + return element + } else if let unlabeled = expr as? ExprSyntax { + return TupleExprElementSyntax( + label: nil, + colon: nil, + expression: unlabeled, + trailingComma: index < parameters.count - 1 + ? .commaToken(trailingTrivia: .space) + : nil + ) + } else { + fatalError("ParameterExp.syntax must return LabeledExprSyntax or ExprSyntax") + } + } + ) + return ExprSyntax( + FunctionCallExprSyntax( + calledExpression: ExprSyntax( + MemberAccessExprSyntax( + base: base, + dot: .periodToken(), + name: method + ) + ), + leftParen: .leftParenToken(), + arguments: args, + rightParen: .rightParenToken() + ) + ) + } +} diff --git a/Sources/SyntaxKit/Infix.swift b/Sources/SyntaxKit/Expressions/Infix.swift similarity index 56% rename from Sources/SyntaxKit/Infix.swift rename to Sources/SyntaxKit/Expressions/Infix.swift index 82ce259..432f82f 100644 --- a/Sources/SyntaxKit/Infix.swift +++ b/Sources/SyntaxKit/Expressions/Infix.swift @@ -31,17 +31,17 @@ import SwiftSyntax /// A generic binary (infix) operator expression, e.g. `a + b`. public struct Infix: CodeBlock { - private let op: String + private let operation: String private let operands: [CodeBlock] /// Creates an infix operator expression. /// - Parameters: - /// - op: The operator symbol as it should appear in source (e.g. "+", "-", "&&"). + /// - operation: The operator symbol as it should appear in source (e.g. "+", "-", "&&"). /// - content: A ``CodeBlockBuilder`` that supplies the two operand expressions. /// /// Exactly two operands must be supplied – a left-hand side and a right-hand side. - public init(_ op: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { - self.op = op + public init(_ operation: String, @CodeBlockBuilderResult _ content: () -> [CodeBlock]) { + self.operation = operation self.operands = content() } @@ -55,7 +55,7 @@ public struct Infix: CodeBlock { let operatorExpr = ExprSyntax( BinaryOperatorExprSyntax( - operator: .binaryOperator(op, leadingTrivia: .space, trailingTrivia: .space) + operator: .binaryOperator(operation, leadingTrivia: .space, trailingTrivia: .space) ) ) @@ -69,52 +69,29 @@ public struct Infix: CodeBlock { } } -// MARK: - Operator Overloads for Infix Expressions +// MARK: - Comparison Operators -/// Creates a greater-than comparison expression. -/// - Parameters: -/// - lhs: The left-hand side expression. -/// - rhs: The right-hand side expression. -/// - Returns: An infix expression representing `lhs > rhs`. -public func > (lhs: CodeBlock, rhs: CodeBlock) -> Infix { - Infix(">") { - lhs - rhs - } -} - -/// Creates a less-than comparison expression. -/// - Parameters: -/// - lhs: The left-hand side expression. -/// - rhs: The right-hand side expression. -/// - Returns: An infix expression representing `lhs < rhs`. -public func < (lhs: CodeBlock, rhs: CodeBlock) -> Infix { - Infix("<") { - lhs - rhs - } -} +extension Infix { + /// Comparison operators that can be used in infix expressions. + public enum ComparisonOperator: String, CaseIterable { + case greaterThan = ">" + case lessThan = "<" + case equal = "==" + case notEqual = "!=" -/// Creates an equality comparison expression. -/// - Parameters: -/// - lhs: The left-hand side expression. -/// - rhs: The right-hand side expression. -/// - Returns: An infix expression representing `lhs == rhs`. -public func == (lhs: CodeBlock, rhs: CodeBlock) -> Infix { - Infix("==") { - lhs - rhs + /// The string representation of the operator. + public var symbol: String { + rawValue + } } -} -/// Creates an inequality comparison expression. -/// - Parameters: -/// - lhs: The left-hand side expression. -/// - rhs: The right-hand side expression. -/// - Returns: An infix expression representing `lhs != rhs`. -public func != (lhs: CodeBlock, rhs: CodeBlock) -> Infix { - Infix("!=") { - lhs - rhs + /// Creates an infix expression with a comparison operator. + /// - Parameters: + /// - operator: The comparison operator to use. + /// - lhs: The left-hand side expression. + /// - rhs: The right-hand side expression. + public init(_ operator: ComparisonOperator, lhs: CodeBlock, rhs: CodeBlock) { + self.operation = `operator`.symbol + self.operands = [lhs, rhs] } } diff --git a/Sources/SyntaxKit/Literal+Convenience.swift b/Sources/SyntaxKit/Expressions/Literal+Convenience.swift similarity index 100% rename from Sources/SyntaxKit/Literal+Convenience.swift rename to Sources/SyntaxKit/Expressions/Literal+Convenience.swift diff --git a/Sources/SyntaxKit/Literal+ExprCodeBlock.swift b/Sources/SyntaxKit/Expressions/Literal+ExprCodeBlock.swift similarity index 77% rename from Sources/SyntaxKit/Literal+ExprCodeBlock.swift rename to Sources/SyntaxKit/Expressions/Literal+ExprCodeBlock.swift index 8b74e74..8dca1b8 100644 --- a/Sources/SyntaxKit/Literal+ExprCodeBlock.swift +++ b/Sources/SyntaxKit/Expressions/Literal+ExprCodeBlock.swift @@ -39,11 +39,14 @@ extension Literal: ExprCodeBlock { return ExprSyntax( StringLiteralExprSyntax( openingQuote: .stringQuoteToken(), - segments: .init([ - .stringSegment(.init(content: .stringSegment(value))) - ]), + segments: .init( + [ + .stringSegment(.init(content: .stringSegment(value))) + ] + ), closingQuote: .stringQuoteToken() - )) + ) + ) case .float(let value): return ExprSyntax(FloatLiteralExprSyntax(literal: .floatLiteral(String(value)))) case .integer(let value): @@ -52,7 +55,10 @@ extension Literal: ExprCodeBlock { return ExprSyntax(NilLiteralExprSyntax(nilKeyword: .keyword(.nil))) case .boolean(let value): return ExprSyntax( - BooleanLiteralExprSyntax(literal: value ? .keyword(.true) : .keyword(.false))) + BooleanLiteralExprSyntax( + literal: value ? .keyword(.true) : .keyword(.false) + ) + ) case .ref(let value): return ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(value))) case .tuple(let elements): @@ -69,7 +75,9 @@ extension Literal: ExprCodeBlock { label: nil, colon: nil, expression: elementExpr, - trailingComma: index < elements.count - 1 ? .commaToken(trailingTrivia: .space) : nil + trailingComma: index < elements.count - 1 + ? .commaToken(trailingTrivia: .space) + : nil ) } ) @@ -78,14 +86,19 @@ extension Literal: ExprCodeBlock { leftParen: .leftParenToken(), elements: tupleElements, rightParen: .rightParenToken() - )) + ) + ) case .array(let elements): let arrayElements = ArrayElementListSyntax( elements.enumerated().map { index, element in ArrayElementSyntax( expression: element.syntax.as(ExprSyntax.self) - ?? ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(""))), - trailingComma: index < elements.count - 1 ? .commaToken(trailingTrivia: .space) : nil + ?? ExprSyntax( + DeclReferenceExprSyntax(baseName: .identifier("")) + ), + trailingComma: index < elements.count - 1 + ? .commaToken(trailingTrivia: .space) + : nil ) } ) @@ -96,9 +109,15 @@ extension Literal: ExprCodeBlock { return ExprSyntax( DictionaryExprSyntax( leftSquare: .leftSquareToken(), - content: .colon(.colonToken(leadingTrivia: .init(), trailingTrivia: .init())), + content: .colon( + .colonToken( + leadingTrivia: .init(), + trailingTrivia: .init() + ) + ), rightSquare: .rightSquareToken() - )) + ) + ) } else { let dictionaryElements = DictionaryElementListSyntax( elements.enumerated().map { index, keyValue in @@ -107,11 +126,17 @@ extension Literal: ExprCodeBlock { keyExpression: key.exprSyntax, colon: .colonToken(), valueExpression: value.exprSyntax, - trailingComma: index < elements.count - 1 ? .commaToken(trailingTrivia: .space) : nil + trailingComma: index < elements.count - 1 + ? .commaToken(trailingTrivia: .space) + : nil ) } ) - return ExprSyntax(DictionaryExprSyntax(content: .elements(dictionaryElements))) + return ExprSyntax( + DictionaryExprSyntax( + content: .elements(dictionaryElements) + ) + ) } } } diff --git a/Sources/SyntaxKit/Expressions/Literal+PatternConvertible.swift b/Sources/SyntaxKit/Expressions/Literal+PatternConvertible.swift new file mode 100644 index 0000000..39b85fc --- /dev/null +++ b/Sources/SyntaxKit/Expressions/Literal+PatternConvertible.swift @@ -0,0 +1,40 @@ +// +// Literal+PatternConvertible.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 + +extension Literal: PatternConvertible { + /// SwiftSyntax representation of the literal as a pattern. + public var patternSyntax: PatternSyntax { + guard let expr = self.syntax.as(ExprSyntax.self) else { + fatalError("Literal.syntax did not return ExprSyntax") + } + return PatternSyntax(ExpressionPatternSyntax(expression: expr)) + } +} diff --git a/Sources/SyntaxKit/Literal.swift b/Sources/SyntaxKit/Expressions/Literal.swift similarity index 100% rename from Sources/SyntaxKit/Literal.swift rename to Sources/SyntaxKit/Expressions/Literal.swift diff --git a/Sources/SyntaxKit/LiteralValue.swift b/Sources/SyntaxKit/Expressions/LiteralValue.swift similarity index 100% rename from Sources/SyntaxKit/LiteralValue.swift rename to Sources/SyntaxKit/Expressions/LiteralValue.swift diff --git a/Sources/SyntaxKit/CatchBuilder.swift b/Sources/SyntaxKit/Expressions/NegatedPropertyAccessExp.swift similarity index 56% rename from Sources/SyntaxKit/CatchBuilder.swift rename to Sources/SyntaxKit/Expressions/NegatedPropertyAccessExp.swift index 6991d2a..7b9e1c2 100644 --- a/Sources/SyntaxKit/CatchBuilder.swift +++ b/Sources/SyntaxKit/Expressions/NegatedPropertyAccessExp.swift @@ -1,5 +1,5 @@ // -// CatchBuilder.swift +// NegatedPropertyAccessExp.swift // SyntaxKit // // Created by Leo Dion. @@ -29,34 +29,36 @@ import SwiftSyntax -/// A result builder for creating catch clauses in a do-catch statement. -@resultBuilder -public struct CatchBuilder { - public static func buildBlock(_ components: CatchClauseSyntax...) -> CatchClauseListSyntax { - CatchClauseListSyntax(components) - } - - public static func buildExpression(_ expression: CatchClauseSyntax) -> CatchClauseSyntax { - expression - } - - public static func buildExpression(_ expression: Catch) -> CatchClauseSyntax { - expression.catchClauseSyntax - } - - public static func buildOptional(_ component: CatchClauseSyntax?) -> CatchClauseSyntax? { - component - } +/// An expression that negates a property access. +public struct NegatedPropertyAccessExp: CodeBlock { + internal let base: CodeBlock - public static func buildEither(first component: CatchClauseSyntax) -> CatchClauseSyntax { - component + /// Creates a negated property access expression. + /// - Parameter base: The base property access expression. + public init(base: CodeBlock) { + self.base = base } - public static func buildEither(second component: CatchClauseSyntax) -> CatchClauseSyntax { - component + /// Backward compatibility initializer for (baseName, propertyName). + public init(baseName: String, propertyName: String) { + self.base = PropertyAccessExp(baseName: baseName, propertyName: propertyName) } - public static func buildArray(_ components: [CatchClauseSyntax]) -> CatchClauseListSyntax { - CatchClauseListSyntax(components) + public var syntax: SyntaxProtocol { + let memberAccess = + base.syntax.as(ExprSyntax.self) + ?? ExprSyntax( + DeclReferenceExprSyntax(baseName: .identifier("")) + ) + return ExprSyntax( + PrefixOperatorExprSyntax( + operator: .prefixOperator( + "!", + leadingTrivia: [], + trailingTrivia: [] + ), + expression: memberAccess + ) + ) } } diff --git a/Sources/SyntaxKit/PlusAssign.swift b/Sources/SyntaxKit/Expressions/PlusAssign.swift similarity index 94% rename from Sources/SyntaxKit/PlusAssign.swift rename to Sources/SyntaxKit/Expressions/PlusAssign.swift index 2ad0bab..7c978e7 100644 --- a/Sources/SyntaxKit/PlusAssign.swift +++ b/Sources/SyntaxKit/Expressions/PlusAssign.swift @@ -67,7 +67,13 @@ public struct PlusAssign: CodeBlock { let left = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(target))) let assign = ExprSyntax( BinaryOperatorExprSyntax( - operator: .binaryOperator("+=", leadingTrivia: .space, trailingTrivia: .space))) + operator: .binaryOperator( + "+=", + leadingTrivia: .space, + trailingTrivia: .space + ) + ) + ) return SequenceExprSyntax( elements: ExprListSyntax([ left, diff --git a/Sources/SyntaxKit/Expressions/PropertyAccessExp.swift b/Sources/SyntaxKit/Expressions/PropertyAccessExp.swift new file mode 100644 index 0000000..a23b53f --- /dev/null +++ b/Sources/SyntaxKit/Expressions/PropertyAccessExp.swift @@ -0,0 +1,80 @@ +// +// PropertyAccessExp.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 + +/// An expression that accesses a property on a base expression. +public struct PropertyAccessExp: CodeBlock { + internal let base: CodeBlock + internal let propertyName: String + + /// Creates a property access expression. + /// - Parameters: + /// - base: The base expression. + /// - propertyName: The name of the property to access. + public init(base: CodeBlock, propertyName: String) { + self.base = base + self.propertyName = propertyName + } + + /// Convenience initializer for backward compatibility (baseName as String). + public init(baseName: String, propertyName: String) { + self.base = VariableExp(baseName) + self.propertyName = propertyName + } + + /// Accesses a property on the current property access expression (chaining). + /// - Parameter propertyName: The name of the next property to access. + /// - Returns: A new ``PropertyAccessExp`` representing the chained property access. + public func property(_ propertyName: String) -> PropertyAccessExp { + PropertyAccessExp(base: self, propertyName: propertyName) + } + + /// Negates the property access expression. + /// - Returns: A negated property access expression. + public func not() -> CodeBlock { + NegatedPropertyAccessExp(base: self) + } + + public var syntax: SyntaxProtocol { + let baseSyntax = + base.syntax.as(ExprSyntax.self) + ?? ExprSyntax( + DeclReferenceExprSyntax(baseName: .identifier("")) + ) + let property = TokenSyntax.identifier(propertyName) + return ExprSyntax( + MemberAccessExprSyntax( + base: baseSyntax, + dot: .periodToken(), + name: property + ) + ) + } +} diff --git a/Sources/SyntaxKit/Return.swift b/Sources/SyntaxKit/Expressions/Return.swift similarity index 100% rename from Sources/SyntaxKit/Return.swift rename to Sources/SyntaxKit/Expressions/Return.swift diff --git a/Sources/SyntaxKit/Function+EffectSpecifiers.swift b/Sources/SyntaxKit/Functions/Function+EffectSpecifiers.swift similarity index 96% rename from Sources/SyntaxKit/Function+EffectSpecifiers.swift rename to Sources/SyntaxKit/Functions/Function+EffectSpecifiers.swift index f985770..0c31748 100644 --- a/Sources/SyntaxKit/Function+EffectSpecifiers.swift +++ b/Sources/SyntaxKit/Functions/Function+EffectSpecifiers.swift @@ -35,7 +35,7 @@ extension Function { switch effect { case .none: return nil - case .throws(let isRethrows, let errorType): + case let .throws(isRethrows, errorType): let throwsSpecifier = buildThrowsSpecifier(isRethrows: isRethrows) if let errorType = errorType { return FunctionEffectSpecifiersSyntax( @@ -53,7 +53,7 @@ extension Function { asyncSpecifier: .keyword(.async, leadingTrivia: .space, trailingTrivia: .space), throwsSpecifier: nil ) - case .asyncThrows(let isRethrows, let errorType): + case let .asyncThrows(isRethrows, errorType): let throwsSpecifier = buildThrowsSpecifier(isRethrows: isRethrows) if let errorType = errorType { return FunctionEffectSpecifiersSyntax( diff --git a/Sources/SyntaxKit/Function+Effects.swift b/Sources/SyntaxKit/Functions/Function+Effects.swift similarity index 100% rename from Sources/SyntaxKit/Function+Effects.swift rename to Sources/SyntaxKit/Functions/Function+Effects.swift diff --git a/Sources/SyntaxKit/Function+Modifiers.swift b/Sources/SyntaxKit/Functions/Function+Modifiers.swift similarity index 100% rename from Sources/SyntaxKit/Function+Modifiers.swift rename to Sources/SyntaxKit/Functions/Function+Modifiers.swift diff --git a/Sources/SyntaxKit/Function+Syntax.swift b/Sources/SyntaxKit/Functions/Function+Syntax.swift similarity index 70% rename from Sources/SyntaxKit/Function+Syntax.swift rename to Sources/SyntaxKit/Functions/Function+Syntax.swift index 5c03603..8bb8796 100644 --- a/Sources/SyntaxKit/Function+Syntax.swift +++ b/Sources/SyntaxKit/Functions/Function+Syntax.swift @@ -37,50 +37,18 @@ extension Function { // Build parameter list let paramList = FunctionParameterListSyntax( - parameters.enumerated().compactMap { index, param in - // Skip empty placeholders (possible in some builder scenarios) - guard !param.name.isEmpty || param.defaultValue != nil else { return nil } - - // Attributes for parameter - let paramAttributes = buildAttributeList(from: param.attributes) - - let firstNameLeading: Trivia = paramAttributes.isEmpty ? [] : .space - - // Determine first & second names - let firstNameToken: TokenSyntax - let secondNameToken: TokenSyntax? - - if param.isUnnamed { - firstNameToken = .wildcardToken(leadingTrivia: firstNameLeading, trailingTrivia: .space) - secondNameToken = .identifier(param.name) - } else if let label = param.label { - firstNameToken = .identifier( - label, leadingTrivia: firstNameLeading, trailingTrivia: .space) - secondNameToken = .identifier(param.name) - } else { - firstNameToken = .identifier( - param.name, leadingTrivia: firstNameLeading, trailingTrivia: .space) - secondNameToken = nil - } - - var paramSyntax = FunctionParameterSyntax( - attributes: paramAttributes, - firstName: firstNameToken, - secondName: secondNameToken, - colon: .colonToken(trailingTrivia: .space), - type: IdentifierTypeSyntax(name: .identifier(param.type)), - defaultValue: param.defaultValue.map { - InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier($0))) - ) - } - ) - if index < parameters.count - 1 { - paramSyntax = paramSyntax.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + parameters + .enumerated() + .compactMap { index, param in + let isLast = index >= parameters.count - 1 + let paramAttributes = buildAttributeList(from: param.attributes) + return FunctionParameterSyntax.create( + from: param, + attributes: paramAttributes, + isLast: isLast + ) } - return paramSyntax - }) + ) // Build return type if specified var returnClause: ReturnClauseSyntax? @@ -105,7 +73,8 @@ extension Function { item = CodeBlockItemSyntax(item: .stmt(stmt)) } return item?.with(\.trailingTrivia, .newline) - }), + } + ), rightBrace: .rightBraceToken(leadingTrivia: .newline) ) @@ -115,15 +84,18 @@ extension Function { // Build modifiers var modifiers: DeclModifierListSyntax = [] if isStatic { - modifiers = DeclModifierListSyntax([ - DeclModifierSyntax(name: .keyword(.static, trailingTrivia: .space)) - ]) + modifiers = DeclModifierListSyntax( + [ + DeclModifierSyntax(name: .keyword(.static, trailingTrivia: .space)) + ] + ) } if isMutating { modifiers = DeclModifierListSyntax( modifiers + [ DeclModifierSyntax(name: .keyword(.mutating, trailingTrivia: .space)) - ]) + ] + ) } return FunctionDeclSyntax( @@ -171,7 +143,10 @@ extension Function { argumentList.enumerated().map { index, expr in var element = LabeledExprSyntax(expression: ExprSyntax(expr)) if index < argumentList.count - 1 { - element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) + element = element.with( + \.trailingComma, + .commaToken(trailingTrivia: .space) + ) } return element } diff --git a/Sources/SyntaxKit/Function.swift b/Sources/SyntaxKit/Functions/Function.swift similarity index 96% rename from Sources/SyntaxKit/Function.swift rename to Sources/SyntaxKit/Functions/Function.swift index 655970b..038eb09 100644 --- a/Sources/SyntaxKit/Function.swift +++ b/Sources/SyntaxKit/Functions/Function.swift @@ -46,7 +46,8 @@ public struct Function: CodeBlock { /// - returnType: The return type of the function, if any. /// - content: A ``CodeBlockBuilder`` that provides the body of the function. public init( - _ name: String, returns returnType: String? = nil, + _ name: String, + returns returnType: String? = nil, @CodeBlockBuilderResult _ content: () -> [CodeBlock] ) { self.name = name @@ -62,7 +63,8 @@ public struct Function: CodeBlock { /// - params: A ``ParameterBuilder`` that provides the parameters of the function. /// - content: A ``CodeBlockBuilder`` that provides the body of the function. public init( - _ name: String, returns returnType: String? = nil, + _ name: String, + returns returnType: String? = nil, @ParameterBuilderResult _ params: () -> [Parameter], @CodeBlockBuilderResult _ content: () -> [CodeBlock] ) { diff --git a/Sources/SyntaxKit/Functions/FunctionParameterSyntax+Init.swift b/Sources/SyntaxKit/Functions/FunctionParameterSyntax+Init.swift new file mode 100644 index 0000000..3289a17 --- /dev/null +++ b/Sources/SyntaxKit/Functions/FunctionParameterSyntax+Init.swift @@ -0,0 +1,114 @@ +// +// FunctionParameterSyntax+Init.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 + +extension FunctionParameterSyntax { + /// Private struct to hold parameter name tokens. + private struct ParameterNames { + let firstNameToken: TokenSyntax + let secondNameToken: TokenSyntax? + + /// Creates parameter name tokens from a parameter. + /// - Parameters: + /// - parameter: The parameter to create tokens for. + /// - firstNameLeading: The leading trivia for the first name token. + init(from parameter: Parameter, firstNameLeading: Trivia) { + if parameter.isUnnamed { + self.firstNameToken = .wildcardToken( + leadingTrivia: firstNameLeading, + trailingTrivia: .space + ) + self.secondNameToken = .identifier(parameter.name) + } else if let label = parameter.label { + self.firstNameToken = .identifier( + label, + leadingTrivia: firstNameLeading, + trailingTrivia: .space + ) + self.secondNameToken = .identifier(parameter.name) + } else { + self.firstNameToken = .identifier( + parameter.name, + leadingTrivia: firstNameLeading, + trailingTrivia: .space + ) + self.secondNameToken = nil + } + } + } + + /// Creates a `FunctionParameterSyntax` from a `Parameter`. + /// - Parameters: + /// - parameter: The parameter to convert. + /// - attributes: The attributes for the parameter. + /// - isLast: Whether this is the last parameter in the list. + /// - Returns: A `FunctionParameterSyntax` if the conversion is successful, `nil` otherwise. + public static func create( + from parameter: Parameter, + attributes: AttributeListSyntax, + isLast: Bool + ) -> FunctionParameterSyntax? { + // Skip empty placeholders (possible in some builder scenarios) + guard !parameter.name.isEmpty || parameter.defaultValue != nil else { + return nil + } + + let firstNameLeading: Trivia = attributes.isEmpty ? [] : .space + let parameterNames = ParameterNames(from: parameter, firstNameLeading: firstNameLeading) + + var paramSyntax = FunctionParameterSyntax( + attributes: attributes, + firstName: parameterNames.firstNameToken, + secondName: parameterNames.secondNameToken, + colon: .colonToken(trailingTrivia: .space), + type: IdentifierTypeSyntax(name: .identifier(parameter.type)), + defaultValue: parameter.defaultValue.map { + InitializerClauseSyntax( + equal: .equalToken( + leadingTrivia: .space, + trailingTrivia: .space + ), + value: ExprSyntax( + DeclReferenceExprSyntax(baseName: .identifier($0)) + ) + ) + } + ) + + if !isLast { + paramSyntax = paramSyntax.with( + \.trailingComma, + .commaToken(trailingTrivia: .space) + ) + } + + return paramSyntax + } +} diff --git a/Sources/SyntaxKit/FunctionRequirement.swift b/Sources/SyntaxKit/Functions/FunctionRequirement.swift similarity index 96% rename from Sources/SyntaxKit/FunctionRequirement.swift rename to Sources/SyntaxKit/Functions/FunctionRequirement.swift index 3faf7ae..6609a9c 100644 --- a/Sources/SyntaxKit/FunctionRequirement.swift +++ b/Sources/SyntaxKit/Functions/FunctionRequirement.swift @@ -53,7 +53,8 @@ public struct FunctionRequirement: CodeBlock { /// - returnType: Optional return type. /// - params: A ParameterBuilderResult providing the parameters. public init( - _ name: String, returns returnType: String? = nil, + _ name: String, + returns returnType: String? = nil, @ParameterBuilderResult _ params: () -> [Parameter] ) { self.name = name @@ -86,7 +87,9 @@ public struct FunctionRequirement: CodeBlock { } else { paramList = FunctionParameterListSyntax( parameters.enumerated().compactMap { index, param in - guard !param.name.isEmpty, !param.type.isEmpty else { return nil } + guard !param.name.isEmpty, !param.type.isEmpty else { + return nil + } var paramSyntax = FunctionParameterSyntax( firstName: param.isUnnamed ? .wildcardToken(trailingTrivia: .space) : .identifier(param.name), @@ -104,7 +107,8 @@ public struct FunctionRequirement: CodeBlock { paramSyntax = paramSyntax.with(\.trailingComma, .commaToken(trailingTrivia: .space)) } return paramSyntax - }) + } + ) } // Return clause diff --git a/Sources/SyntaxKit/If.swift b/Sources/SyntaxKit/If.swift deleted file mode 100644 index b916df7..0000000 --- a/Sources/SyntaxKit/If.swift +++ /dev/null @@ -1,224 +0,0 @@ -// -// If.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 - -/// An `if` statement. -public struct If: CodeBlock { - private let conditions: [CodeBlock] - private let body: [CodeBlock] - private let elseBody: [CodeBlock]? - - /// Creates an `if` statement with optional `else`. - /// - Parameters: - /// - condition: A single `CodeBlock` produced by the builder that describes the `if` condition. - /// - then: Builder that produces the body for the `if` branch. - /// - elseBody: Builder that produces the body for the `else` branch. The body may contain - /// nested `If` instances (representing `else if`) and/or a ``Then`` block for the - /// final `else` statements. - public init( - @CodeBlockBuilderResult _ condition: () -> [CodeBlock], - @CodeBlockBuilderResult then: () -> [CodeBlock], - @CodeBlockBuilderResult else elseBody: () -> [CodeBlock] = { [] } - ) { - let allConditions = condition() - guard !allConditions.isEmpty else { - fatalError("If requires at least one condition CodeBlock") - } - self.conditions = allConditions - self.body = then() - let generatedElse = elseBody() - self.elseBody = generatedElse.isEmpty ? nil : generatedElse - } - - /// Convenience initializer that keeps the previous API: pass the condition directly. - public init( - _ condition: CodeBlock, - @CodeBlockBuilderResult then: () -> [CodeBlock], - @CodeBlockBuilderResult else elseBody: () -> [CodeBlock] = { [] } - ) { - self.init({ condition }, then: then, else: elseBody) - } - - public var syntax: SyntaxProtocol { - // Build list of ConditionElements from all provided conditions - let condList = ConditionElementListSyntax( - conditions.enumerated().map { index, block in - let needsComma = index < conditions.count - 1 - - func appendComma(_ element: ConditionElementSyntax) -> ConditionElementSyntax { - needsComma ? element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) : element - } - - if let letCond = block as? Let { - let element = ConditionElementSyntax( - condition: .optionalBinding( - OptionalBindingConditionSyntax( - bindingSpecifier: .keyword(.let, trailingTrivia: .space), - pattern: IdentifierPatternSyntax(identifier: .identifier(letCond.name)), - initializer: InitializerClauseSyntax( - equal: .equalToken(leadingTrivia: .space, trailingTrivia: .space), - value: letCond.value.syntax.as(ExprSyntax.self) - ?? ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(""))) - ) - ) - ) - ) - return appendComma(element) - } else { - let element = ConditionElementSyntax( - condition: .expression( - ExprSyntax( - fromProtocol: block.syntax.as(ExprSyntax.self) - ?? DeclReferenceExprSyntax(baseName: .identifier("")))) - ) - return appendComma(element) - } - } - ) - let bodyBlock = CodeBlockSyntax( - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - statements: CodeBlockItemListSyntax( - body.compactMap { - var item: CodeBlockItemSyntax? - if let enumCase = $0 as? EnumCase { - // Handle EnumCase specially - use expression syntax for enum cases in expressions - item = CodeBlockItemSyntax(item: .expr(enumCase.exprSyntax)) - } else if let decl = $0.syntax.as(DeclSyntax.self) { - item = CodeBlockItemSyntax(item: .decl(decl)) - } else if let expr = $0.syntax.as(ExprSyntax.self) { - item = CodeBlockItemSyntax(item: .expr(expr)) - } else if let stmt = $0.syntax.as(StmtSyntax.self) { - item = CodeBlockItemSyntax(item: .stmt(stmt)) - } - return item?.with(\.trailingTrivia, .newline) - }), - rightBrace: .rightBraceToken(leadingTrivia: .newline) - ) - // swiftlint:disable:next closure_body_length - let elseBlock: IfExprSyntax.ElseBody? = { - guard let elseBlocks = elseBody else { - return nil - } - - // Build a chained else-if structure if the builder provided If blocks. - var current: SyntaxProtocol? - - for block in elseBlocks.reversed() { - switch block { - case let thenBlock as Then: - // Leaf `else` – produce a code-block. - let stmts = CodeBlockItemListSyntax( - thenBlock.body.compactMap { element in - if let enumCase = element as? EnumCase { - // Handle EnumCase specially - use expression syntax for enum cases in expressions - return CodeBlockItemSyntax(item: .expr(enumCase.exprSyntax)).with( - \.trailingTrivia, .newline) - } else if let decl = element.syntax.as(DeclSyntax.self) { - return CodeBlockItemSyntax(item: .decl(decl)).with(\.trailingTrivia, .newline) - } else if let expr = element.syntax.as(ExprSyntax.self) { - return CodeBlockItemSyntax(item: .expr(expr)).with(\.trailingTrivia, .newline) - } else if let stmt = element.syntax.as(StmtSyntax.self) { - return CodeBlockItemSyntax(item: .stmt(stmt)).with(\.trailingTrivia, .newline) - } - return nil - }) - let codeBlock = CodeBlockSyntax( - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - statements: stmts, - rightBrace: .rightBraceToken(leadingTrivia: .newline) - ) - current = codeBlock as SyntaxProtocol - - case let ifBlock as If: - guard var ifExpr = ifBlock.syntax.as(IfExprSyntax.self) else { continue } - if let nested = current { - let elseChoice: IfExprSyntax.ElseBody - if let cb = nested.as(CodeBlockSyntax.self) { - elseChoice = IfExprSyntax.ElseBody(cb) - } else if let nestedIf = nested.as(IfExprSyntax.self) { - elseChoice = IfExprSyntax.ElseBody(nestedIf) - } else { - continue - } - - ifExpr = - ifExpr - .with(\.elseKeyword, .keyword(.else, leadingTrivia: .space, trailingTrivia: .space)) - .with(\.elseBody, elseChoice) - } - current = ifExpr as SyntaxProtocol - - default: - // Treat any other CodeBlock as part of a final code-block - let item: CodeBlockItemSyntax? - if let enumCase = block as? EnumCase { - // Handle EnumCase specially - use expression syntax for enum cases in expressions - item = CodeBlockItemSyntax(item: .expr(enumCase.exprSyntax)) - } else if let decl = block.syntax.as(DeclSyntax.self) { - item = CodeBlockItemSyntax(item: .decl(decl)) - } else if let expr = block.syntax.as(ExprSyntax.self) { - item = CodeBlockItemSyntax(item: .expr(expr)) - } else if let stmt = block.syntax.as(StmtSyntax.self) { - item = CodeBlockItemSyntax(item: .stmt(stmt)) - } else { - item = nil - } - if let itm = item { - let codeBlock = CodeBlockSyntax( - leftBrace: .leftBraceToken(leadingTrivia: .space, trailingTrivia: .newline), - statements: CodeBlockItemListSyntax([itm.with(\.trailingTrivia, .newline)]), - rightBrace: .rightBraceToken(leadingTrivia: .newline) - ) - current = codeBlock as SyntaxProtocol - } - } - } - - if let final = current { - if let cb = final.as(CodeBlockSyntax.self) { - return IfExprSyntax.ElseBody(cb) - } else if let ifExpr = final.as(IfExprSyntax.self) { - return IfExprSyntax.ElseBody(ifExpr) - } - } - return nil - }() - return ExprSyntax( - IfExprSyntax( - ifKeyword: .keyword(.if, trailingTrivia: .space), - conditions: condList, - body: bodyBlock, - elseKeyword: elseBlock != nil - ? .keyword(.else, leadingTrivia: .space, trailingTrivia: .space) : nil, - elseBody: elseBlock - ) - ) - } -} diff --git a/Sources/SyntaxKit/Parameter.swift b/Sources/SyntaxKit/Parameters/Parameter.swift similarity index 92% rename from Sources/SyntaxKit/Parameter.swift rename to Sources/SyntaxKit/Parameters/Parameter.swift index a5fa4a7..7055911 100644 --- a/Sources/SyntaxKit/Parameter.swift +++ b/Sources/SyntaxKit/Parameters/Parameter.swift @@ -49,14 +49,6 @@ public struct Parameter: CodeBlock { internal var attributes: [AttributeInfo] = [] - /// Creates a parameter for a function or initializer. - /// - Parameters: - /// - name: The name of the parameter. - /// - type: The type of the parameter. - /// - defaultValue: The default value of the parameter, if any. - /// - isUnnamed: A Boolean value that indicates whether the parameter is unnamed. - // NOTE: The previous initializer that accepted an `isUnnamed` flag has been replaced. - /// Creates an unlabeled parameter for function calls or initializers. /// - Parameter value: The value of the parameter. public init(unlabeled value: String) { diff --git a/Sources/SyntaxKit/ParameterBuilderResult.swift b/Sources/SyntaxKit/Parameters/ParameterBuilderResult.swift similarity index 100% rename from Sources/SyntaxKit/ParameterBuilderResult.swift rename to Sources/SyntaxKit/Parameters/ParameterBuilderResult.swift diff --git a/Sources/SyntaxKit/ParameterExp.swift b/Sources/SyntaxKit/Parameters/ParameterExp.swift similarity index 100% rename from Sources/SyntaxKit/ParameterExp.swift rename to Sources/SyntaxKit/Parameters/ParameterExp.swift diff --git a/Sources/SyntaxKit/ParameterExpBuilderResult.swift b/Sources/SyntaxKit/Parameters/ParameterExpBuilderResult.swift similarity index 100% rename from Sources/SyntaxKit/ParameterExpBuilderResult.swift rename to Sources/SyntaxKit/Parameters/ParameterExpBuilderResult.swift diff --git a/Sources/SyntaxKit/PatternConvertible.swift b/Sources/SyntaxKit/PatternConvertible.swift deleted file mode 100644 index 7e502e4..0000000 --- a/Sources/SyntaxKit/PatternConvertible.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// PatternConvertible.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 Foundation -import SwiftSyntax - -/// Types that can be turned into a `PatternSyntax` suitable for a `switch` case pattern. -public protocol PatternConvertible { - /// SwiftSyntax representation of the pattern. - var patternSyntax: PatternSyntax { get } -} - -// MARK: - Literal conformance - -extension Literal: PatternConvertible { - /// SwiftSyntax representation of the literal as a pattern. - public var patternSyntax: PatternSyntax { - guard let expr = self.syntax.as(ExprSyntax.self) else { - fatalError("Literal.syntax did not return ExprSyntax") - } - return PatternSyntax(ExpressionPatternSyntax(expression: expr)) - } -} - -// MARK: - Int conformance - -extension Int: PatternConvertible { - /// SwiftSyntax representation of the integer as a pattern. - public var patternSyntax: PatternSyntax { - let expr = ExprSyntax(IntegerLiteralExprSyntax(literal: .integerLiteral(String(self)))) - return PatternSyntax(ExpressionPatternSyntax(expression: expr)) - } -} - -// MARK: - Ranges - -extension Swift.Range: PatternConvertible where Bound == Int { - /// SwiftSyntax representation of the range as a pattern. - public var patternSyntax: PatternSyntax { - let lhs = ExprSyntax( - IntegerLiteralExprSyntax(literal: .integerLiteral(String(self.lowerBound)))) - let op = ExprSyntax(BinaryOperatorExprSyntax(operator: .binaryOperator("..<"))) - let rhs = ExprSyntax( - IntegerLiteralExprSyntax(literal: .integerLiteral(String(self.upperBound)))) - let seq = SequenceExprSyntax(elements: ExprListSyntax([lhs, op, rhs])) - return PatternSyntax(ExpressionPatternSyntax(expression: ExprSyntax(seq))) - } -} - -extension Swift.ClosedRange: PatternConvertible where Bound == Int { - /// SwiftSyntax representation of the closed range as a pattern. - public var patternSyntax: PatternSyntax { - let lhs = ExprSyntax( - IntegerLiteralExprSyntax(literal: .integerLiteral(String(self.lowerBound)))) - let op = ExprSyntax(BinaryOperatorExprSyntax(operator: .binaryOperator("..."))) - let rhs = ExprSyntax( - IntegerLiteralExprSyntax(literal: .integerLiteral(String(self.upperBound)))) - let seq = SequenceExprSyntax(elements: ExprListSyntax([lhs, op, rhs])) - return PatternSyntax(ExpressionPatternSyntax(expression: ExprSyntax(seq))) - } -} - -// MARK: - String identifiers - -extension String: PatternConvertible { - /// SwiftSyntax representation of the string as an identifier pattern. - public var patternSyntax: PatternSyntax { - PatternSyntax(IdentifierPatternSyntax(identifier: .identifier(self))) - } -} - -// MARK: - Let binding pattern - -/// A `let` binding pattern for switch cases. -public struct LetBindingPattern: PatternConvertible { - private let identifier: String - - internal init(identifier: String) { - self.identifier = identifier - } - - /// SwiftSyntax representation of the let binding pattern. - public var patternSyntax: PatternSyntax { - PatternSyntax( - ValueBindingPatternSyntax( - bindingSpecifier: .keyword(.let, trailingTrivia: .space), - pattern: PatternSyntax(IdentifierPatternSyntax(identifier: .identifier(identifier))) - ) - ) - } -} - -/// Namespace for pattern creation utilities. -public enum Pattern { - /// Creates a `let` binding pattern for switch cases. - /// - Parameter identifier: The name of the variable to bind. - /// - Returns: A pattern that binds the value to the given identifier. - public static func `let`(_ identifier: String) -> LetBindingPattern { - LetBindingPattern(identifier: identifier) - } -} diff --git a/Sources/SyntaxKit/Patterns/Int+PatternConvertible.swift b/Sources/SyntaxKit/Patterns/Int+PatternConvertible.swift new file mode 100644 index 0000000..e26df13 --- /dev/null +++ b/Sources/SyntaxKit/Patterns/Int+PatternConvertible.swift @@ -0,0 +1,38 @@ +// +// Int+PatternConvertible.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 + +extension Int: PatternConvertible { + /// SwiftSyntax representation of the integer as a pattern. + public var patternSyntax: PatternSyntax { + let expr = ExprSyntax(IntegerLiteralExprSyntax(literal: .integerLiteral(String(self)))) + return PatternSyntax(ExpressionPatternSyntax(expression: expr)) + } +} diff --git a/Sources/SyntaxKit/Patterns/LetBindingPattern.swift b/Sources/SyntaxKit/Patterns/LetBindingPattern.swift new file mode 100644 index 0000000..d9f3f5a --- /dev/null +++ b/Sources/SyntaxKit/Patterns/LetBindingPattern.swift @@ -0,0 +1,61 @@ +// +// LetBindingPattern.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 + +// MARK: - Let binding pattern + +/// Namespace for pattern creation utilities. +public enum Pattern { + /// Creates a `let` binding pattern for switch cases. + /// - Parameter identifier: The name of the variable to bind. + /// - Returns: A pattern that binds the value to the given identifier. + public static func `let`(_ identifier: String) -> LetBindingPattern { + LetBindingPattern(identifier: identifier) + } +} + +/// A `let` binding pattern for switch cases. +public struct LetBindingPattern: PatternConvertible { + private let identifier: String + + internal init(identifier: String) { + self.identifier = identifier + } + + /// SwiftSyntax representation of the let binding pattern. + public var patternSyntax: PatternSyntax { + PatternSyntax( + ValueBindingPatternSyntax( + bindingSpecifier: .keyword(.let, trailingTrivia: .space), + pattern: PatternSyntax(IdentifierPatternSyntax(identifier: .identifier(identifier))) + ) + ) + } +} diff --git a/Sources/SyntaxKit/Patterns/Range+PatternConvertible.swift b/Sources/SyntaxKit/Patterns/Range+PatternConvertible.swift new file mode 100644 index 0000000..0f4a076 --- /dev/null +++ b/Sources/SyntaxKit/Patterns/Range+PatternConvertible.swift @@ -0,0 +1,68 @@ +// +// Range+PatternConvertible.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 + +extension Range: PatternConvertible where Bound == Int { + /// SwiftSyntax representation of the range as a pattern. + public var patternSyntax: PatternSyntax { + let lhs = ExprSyntax( + IntegerLiteralExprSyntax(literal: .integerLiteral(String(self.lowerBound))) + ) + let operation = ExprSyntax( + BinaryOperatorExprSyntax(operator: .binaryOperator("..<")) + ) + let rhs = ExprSyntax( + IntegerLiteralExprSyntax(literal: .integerLiteral(String(self.upperBound))) + ) + let seq = SequenceExprSyntax( + elements: ExprListSyntax([lhs, operation, rhs]) + ) + return PatternSyntax(ExpressionPatternSyntax(expression: ExprSyntax(seq))) + } +} + +extension ClosedRange: PatternConvertible where Bound == Int { + /// SwiftSyntax representation of the closed range as a pattern. + public var patternSyntax: PatternSyntax { + let lhs = ExprSyntax( + IntegerLiteralExprSyntax(literal: .integerLiteral(String(self.lowerBound))) + ) + let operation = ExprSyntax( + BinaryOperatorExprSyntax(operator: .binaryOperator("...")) + ) + let rhs = ExprSyntax( + IntegerLiteralExprSyntax(literal: .integerLiteral(String(self.upperBound))) + ) + let seq = SequenceExprSyntax( + elements: ExprListSyntax([lhs, operation, rhs]) + ) + return PatternSyntax(ExpressionPatternSyntax(expression: ExprSyntax(seq))) + } +} diff --git a/Sources/SyntaxKit/Patterns/String+PatternConvertible.swift b/Sources/SyntaxKit/Patterns/String+PatternConvertible.swift new file mode 100644 index 0000000..944a50e --- /dev/null +++ b/Sources/SyntaxKit/Patterns/String+PatternConvertible.swift @@ -0,0 +1,37 @@ +// +// String+PatternConvertible.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 + +extension String: PatternConvertible { + /// SwiftSyntax representation of the string as an identifier pattern. + public var patternSyntax: PatternSyntax { + PatternSyntax(IdentifierPatternSyntax(identifier: .identifier(self))) + } +} diff --git a/Sources/SyntaxKit/Break.swift b/Sources/SyntaxKit/Utilities/Break.swift similarity index 100% rename from Sources/SyntaxKit/Break.swift rename to Sources/SyntaxKit/Utilities/Break.swift diff --git a/Sources/SyntaxKit/Case.swift b/Sources/SyntaxKit/Utilities/Case.swift similarity index 99% rename from Sources/SyntaxKit/Case.swift rename to Sources/SyntaxKit/Utilities/Case.swift index 14951df..9f5eb7a 100644 --- a/Sources/SyntaxKit/Case.swift +++ b/Sources/SyntaxKit/Utilities/Case.swift @@ -79,7 +79,8 @@ public struct Case: CodeBlock { item = item.with(\.trailingComma, .commaToken(trailingTrivia: .space)) } return item - }) + } + ) let statements = CodeBlockItemListSyntax( body.compactMap { @@ -92,7 +93,8 @@ public struct Case: CodeBlock { item = CodeBlockItemSyntax(item: .stmt(stmt)) } return item?.with(\.trailingTrivia, .newline) - }) + } + ) let label = SwitchCaseLabelSyntax( caseKeyword: .keyword(.case, trailingTrivia: .space), caseItems: caseItems, diff --git a/Sources/SyntaxKit/CommentBuilderResult.swift b/Sources/SyntaxKit/Utilities/CommentBuilderResult.swift similarity index 100% rename from Sources/SyntaxKit/CommentBuilderResult.swift rename to Sources/SyntaxKit/Utilities/CommentBuilderResult.swift diff --git a/Sources/SyntaxKit/Continue.swift b/Sources/SyntaxKit/Utilities/Continue.swift similarity index 100% rename from Sources/SyntaxKit/Continue.swift rename to Sources/SyntaxKit/Utilities/Continue.swift diff --git a/Sources/SyntaxKit/Default.swift b/Sources/SyntaxKit/Utilities/Default.swift similarity index 100% rename from Sources/SyntaxKit/Default.swift rename to Sources/SyntaxKit/Utilities/Default.swift diff --git a/Sources/SyntaxKit/EnumCase+Syntax.swift b/Sources/SyntaxKit/Utilities/EnumCase+Syntax.swift similarity index 100% rename from Sources/SyntaxKit/EnumCase+Syntax.swift rename to Sources/SyntaxKit/Utilities/EnumCase+Syntax.swift diff --git a/Sources/SyntaxKit/EnumCase.swift b/Sources/SyntaxKit/Utilities/EnumCase.swift similarity index 98% rename from Sources/SyntaxKit/EnumCase.swift rename to Sources/SyntaxKit/Utilities/EnumCase.swift index 9a17724..5588426 100644 --- a/Sources/SyntaxKit/EnumCase.swift +++ b/Sources/SyntaxKit/Utilities/EnumCase.swift @@ -111,7 +111,8 @@ public struct EnumCase: CodeBlock { label: nil, colon: nil, expression: ExprSyntax( - DeclReferenceExprSyntax(baseName: .identifier(associated.name))), + DeclReferenceExprSyntax(baseName: .identifier(associated.name)) + ), trailingComma: nil ) } @@ -124,7 +125,8 @@ public struct EnumCase: CodeBlock { leftParen: tuple.leftParen, arguments: tuple.elements, rightParen: tuple.rightParen - )) + ) + ) } else { return ExprSyntax(memberAccess) } diff --git a/Sources/SyntaxKit/Fallthrough.swift b/Sources/SyntaxKit/Utilities/Fallthrough.swift similarity index 100% rename from Sources/SyntaxKit/Fallthrough.swift rename to Sources/SyntaxKit/Utilities/Fallthrough.swift diff --git a/Sources/SyntaxKit/Group.swift b/Sources/SyntaxKit/Utilities/Group.swift similarity index 100% rename from Sources/SyntaxKit/Group.swift rename to Sources/SyntaxKit/Utilities/Group.swift diff --git a/Sources/SyntaxKit/Let.swift b/Sources/SyntaxKit/Utilities/Let.swift similarity index 100% rename from Sources/SyntaxKit/Let.swift rename to Sources/SyntaxKit/Utilities/Let.swift diff --git a/Sources/SyntaxKit/Parenthesized.swift b/Sources/SyntaxKit/Utilities/Parenthesized.swift similarity index 100% rename from Sources/SyntaxKit/Parenthesized.swift rename to Sources/SyntaxKit/Utilities/Parenthesized.swift diff --git a/Sources/SyntaxKit/PropertyRequirement.swift b/Sources/SyntaxKit/Utilities/PropertyRequirement.swift similarity index 100% rename from Sources/SyntaxKit/PropertyRequirement.swift rename to Sources/SyntaxKit/Utilities/PropertyRequirement.swift diff --git a/Sources/SyntaxKit/Then.swift b/Sources/SyntaxKit/Utilities/Then.swift similarity index 100% rename from Sources/SyntaxKit/Then.swift rename to Sources/SyntaxKit/Utilities/Then.swift diff --git a/Sources/SyntaxKit/VariableExp.swift b/Sources/SyntaxKit/VariableExp.swift deleted file mode 100644 index 6870691..0000000 --- a/Sources/SyntaxKit/VariableExp.swift +++ /dev/null @@ -1,214 +0,0 @@ -// -// VariableExp.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 - -/// An expression that refers to a variable. -public struct VariableExp: CodeBlock, PatternConvertible { - internal let name: String - - /// Creates a variable expression. - /// - Parameter name: The name of the variable. - public init(_ name: String) { - self.name = name - } - - /// Accesses a property on the variable. - /// - Parameter propertyName: The name of the property to access. - /// - Returns: A ``PropertyAccessExp`` that represents the property access. - public func property(_ propertyName: String) -> PropertyAccessExp { - PropertyAccessExp(base: self, propertyName: propertyName) - } - - /// Calls a method on the variable. - /// - Parameter methodName: The name of the method to call. - /// - Returns: A ``FunctionCallExp`` that represents the method call. - public func call(_ methodName: String) -> CodeBlock { - FunctionCallExp(baseName: name, methodName: methodName) - } - - /// Calls a method on the variable with parameters. - /// - Parameters: - /// - methodName: The name of the method to call. - /// - params: A ``ParameterExpBuilder`` that provides the parameters for the method call. - /// - Returns: A ``FunctionCallExp`` that represents the method call. - public func call(_ methodName: String, @ParameterExpBuilderResult _ params: () -> [ParameterExp]) - -> CodeBlock - { - FunctionCallExp(baseName: name, methodName: methodName, parameters: params()) - } - - public var syntax: SyntaxProtocol { - ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(name))) - } - - public var patternSyntax: PatternSyntax { - PatternSyntax(IdentifierPatternSyntax(identifier: .identifier(name))) - } -} - -/// An expression that accesses a property on a base expression. -public struct PropertyAccessExp: CodeBlock { - internal let base: CodeBlock - internal let propertyName: String - - /// Creates a property access expression. - /// - Parameters: - /// - base: The base expression. - /// - propertyName: The name of the property to access. - public init(base: CodeBlock, propertyName: String) { - self.base = base - self.propertyName = propertyName - } - - /// Convenience initializer for backward compatibility (baseName as String). - public init(baseName: String, propertyName: String) { - self.base = VariableExp(baseName) - self.propertyName = propertyName - } - - /// Accesses a property on the current property access expression (chaining). - /// - Parameter propertyName: The name of the next property to access. - /// - Returns: A new ``PropertyAccessExp`` representing the chained property access. - public func property(_ propertyName: String) -> PropertyAccessExp { - PropertyAccessExp(base: self, propertyName: propertyName) - } - - /// Negates the property access expression. - /// - Returns: A negated property access expression. - public func not() -> CodeBlock { - NegatedPropertyAccessExp(base: self) - } - - public var syntax: SyntaxProtocol { - let baseSyntax = - base.syntax.as(ExprSyntax.self) - ?? ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(""))) - let property = TokenSyntax.identifier(propertyName) - return ExprSyntax( - MemberAccessExprSyntax( - base: baseSyntax, - dot: .periodToken(), - name: property - )) - } -} - -/// An expression that negates a property access. -public struct NegatedPropertyAccessExp: CodeBlock { - internal let base: CodeBlock - - /// Creates a negated property access expression. - /// - Parameter base: The base property access expression. - public init(base: CodeBlock) { - self.base = base - } - - /// Backward compatibility initializer for (baseName, propertyName). - public init(baseName: String, propertyName: String) { - self.base = PropertyAccessExp(baseName: baseName, propertyName: propertyName) - } - - public var syntax: SyntaxProtocol { - let memberAccess = - base.syntax.as(ExprSyntax.self) - ?? ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(""))) - return ExprSyntax( - PrefixOperatorExprSyntax( - operator: .prefixOperator("!", leadingTrivia: [], trailingTrivia: []), - expression: memberAccess - ) - ) - } -} - -/// An expression that calls a function. -public struct FunctionCallExp: CodeBlock { - internal let baseName: String - internal let methodName: String - internal let parameters: [ParameterExp] - - /// Creates a function call expression. - /// - Parameters: - /// - baseName: The name of the base variable. - /// - methodName: The name of the method to call. - public init(baseName: String, methodName: String) { - self.baseName = baseName - self.methodName = methodName - self.parameters = [] - } - - /// Creates a function call expression with parameters. - /// - Parameters: - /// - baseName: The name of the base variable. - /// - methodName: The name of the method to call. - /// - parameters: The parameters for the method call. - public init(baseName: String, methodName: String, parameters: [ParameterExp]) { - self.baseName = baseName - self.methodName = methodName - self.parameters = parameters - } - - public var syntax: SyntaxProtocol { - let base = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(baseName))) - let method = TokenSyntax.identifier(methodName) - let args = LabeledExprListSyntax( - parameters.enumerated().map { index, param in - let expr = param.syntax - if let labeled = expr as? LabeledExprSyntax { - var element = labeled - if index < parameters.count - 1 { - element = element.with(\.trailingComma, .commaToken(trailingTrivia: .space)) - } - return element - } else if let unlabeled = expr as? ExprSyntax { - return TupleExprElementSyntax( - label: nil, - colon: nil, - expression: unlabeled, - trailingComma: index < parameters.count - 1 ? .commaToken(trailingTrivia: .space) : nil - ) - } else { - fatalError("ParameterExp.syntax must return LabeledExprSyntax or ExprSyntax") - } - }) - return ExprSyntax( - FunctionCallExprSyntax( - calledExpression: ExprSyntax( - MemberAccessExprSyntax( - base: base, - dot: .periodToken(), - name: method - )), - leftParen: .leftParenToken(), - arguments: args, - rightParen: .rightParenToken() - )) - } -} diff --git a/Sources/SyntaxKit/ComputedProperty.swift b/Sources/SyntaxKit/Variables/ComputedProperty.swift similarity index 99% rename from Sources/SyntaxKit/ComputedProperty.swift rename to Sources/SyntaxKit/Variables/ComputedProperty.swift index d78e040..2322642 100644 --- a/Sources/SyntaxKit/ComputedProperty.swift +++ b/Sources/SyntaxKit/Variables/ComputedProperty.swift @@ -61,7 +61,9 @@ public struct ComputedProperty: CodeBlock { item = CodeBlockItemSyntax(item: .stmt(stmt)) } return item?.with(\.trailingTrivia, .newline) - })), + } + ) + ), rightBrace: TokenSyntax.rightBraceToken(leadingTrivia: .newline) ) let identifier = TokenSyntax.identifier(name, trailingTrivia: .space) diff --git a/Sources/SyntaxKit/Variable+LiteralInitializers.swift b/Sources/SyntaxKit/Variables/Variable+LiteralInitializers.swift similarity index 97% rename from Sources/SyntaxKit/Variable+LiteralInitializers.swift rename to Sources/SyntaxKit/Variables/Variable+LiteralInitializers.swift index a23ed4e..1b4fc38 100644 --- a/Sources/SyntaxKit/Variable+LiteralInitializers.swift +++ b/Sources/SyntaxKit/Variables/Variable+LiteralInitializers.swift @@ -32,6 +32,7 @@ import Foundation // MARK: - Variable Literal Initializers extension Variable { + // swiftlint:disable cyclomatic_complexity /// Creates a `let` or `var` declaration with a literal value. /// - Parameters: /// - kind: The kind of variable, either ``VariableKind/let`` or ``VariableKind/var``. @@ -63,7 +64,8 @@ extension Variable { // For any other LiteralValue type that doesn't conform to CodeBlock, // create a fallback or throw an error fatalError( - "Variable: Unsupported LiteralValue type that doesn't conform to CodeBlock: \(T.self)") + "Variable: Unsupported LiteralValue type that doesn't conform to CodeBlock: \(T.self)" + ) } self.init( @@ -75,6 +77,8 @@ extension Variable { ) } + // swiftlint:enable cyclomatic_complexity + /// Creates a `let` or `var` declaration with a string literal value. /// - Parameters: /// - kind: The kind of variable, either ``VariableKind/let`` or ``VariableKind/var``. diff --git a/Sources/SyntaxKit/Variable+TypedInitializers.swift b/Sources/SyntaxKit/Variables/Variable+TypedInitializers.swift similarity index 88% rename from Sources/SyntaxKit/Variable+TypedInitializers.swift rename to Sources/SyntaxKit/Variables/Variable+TypedInitializers.swift index f5f805b..b84e5d6 100644 --- a/Sources/SyntaxKit/Variable+TypedInitializers.swift +++ b/Sources/SyntaxKit/Variables/Variable+TypedInitializers.swift @@ -31,6 +31,7 @@ import Foundation // MARK: - Variable Typed Initializers +// swiftlint:disable discouraged_optional_boolean extension Variable { /// Creates a `let` or `var` declaration with an Init value, inferring the type from the Init. /// - Parameters: @@ -39,7 +40,9 @@ extension Variable { /// - equals: An Init expression. /// - explicitType: Whether the variable has an explicit type. public init( - _ kind: VariableKind, name: String, equals defaultValue: Init, + _ kind: VariableKind, + name: String, + equals defaultValue: Init, explicitType: Bool? = nil ) { self.init( @@ -59,7 +62,10 @@ extension Variable { /// - equals: The initial value expression of the variable, if any. /// - explicitType: Whether the variable has an explicit type. public init( - _ kind: VariableKind, name: String, type: String, equals defaultValue: CodeBlock? = nil, + _ kind: VariableKind, + name: String, + type: String, + equals defaultValue: CodeBlock? = nil, explicitType: Bool? = nil ) { let finalExplicitType = explicitType ?? (defaultValue == nil) @@ -80,7 +86,10 @@ extension Variable { /// - equals: A string literal value. /// - explicitType: Whether the variable has an explicit type. public init( - _ kind: VariableKind, name: String, type: String, equals value: String, + _ kind: VariableKind, + name: String, + type: String, + equals value: String, explicitType: Bool? = nil ) { self.init( @@ -100,7 +109,10 @@ extension Variable { /// - equals: An integer literal value. /// - explicitType: Whether the variable has an explicit type. public init( - _ kind: VariableKind, name: String, type: String, equals value: Int, + _ kind: VariableKind, + name: String, + type: String, + equals value: Int, explicitType: Bool? = nil ) { self.init( @@ -120,7 +132,10 @@ extension Variable { /// - equals: A boolean literal value. /// - explicitType: Whether the variable has an explicit type. public init( - _ kind: VariableKind, name: String, type: String, equals value: Bool, + _ kind: VariableKind, + name: String, + type: String, + equals value: Bool, explicitType: Bool? = nil ) { self.init( @@ -140,7 +155,10 @@ extension Variable { /// - equals: A double literal value. /// - explicitType: Whether the variable has an explicit type. public init( - _ kind: VariableKind, name: String, type: String, equals value: Double, + _ kind: VariableKind, + name: String, + type: String, + equals value: Double, explicitType: Bool? = nil ) { self.init( @@ -152,3 +170,4 @@ extension Variable { ) } } +// swiftlint:enable discouraged_optional_boolean diff --git a/Sources/SyntaxKit/Variable.swift b/Sources/SyntaxKit/Variables/Variable.swift similarity index 100% rename from Sources/SyntaxKit/Variable.swift rename to Sources/SyntaxKit/Variables/Variable.swift diff --git a/Sources/SyntaxKit/VariableDecl.swift b/Sources/SyntaxKit/Variables/VariableDecl.swift similarity index 100% rename from Sources/SyntaxKit/VariableDecl.swift rename to Sources/SyntaxKit/Variables/VariableDecl.swift diff --git a/Sources/SyntaxKit/Variables/VariableExp.swift b/Sources/SyntaxKit/Variables/VariableExp.swift new file mode 100644 index 0000000..8e559aa --- /dev/null +++ b/Sources/SyntaxKit/Variables/VariableExp.swift @@ -0,0 +1,74 @@ +// +// VariableExp.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 + +/// An expression that refers to a variable. +public struct VariableExp: CodeBlock, PatternConvertible { + internal let name: String + + /// Creates a variable expression. + /// - Parameter name: The name of the variable. + public init(_ name: String) { + self.name = name + } + + /// Accesses a property on the variable. + /// - Parameter propertyName: The name of the property to access. + /// - Returns: A ``PropertyAccessExp`` that represents the property access. + public func property(_ propertyName: String) -> PropertyAccessExp { + PropertyAccessExp(base: self, propertyName: propertyName) + } + + /// Calls a method on the variable. + /// - Parameter methodName: The name of the method to call. + /// - Returns: A ``FunctionCallExp`` that represents the method call. + public func call(_ methodName: String) -> CodeBlock { + FunctionCallExp(baseName: name, methodName: methodName) + } + + /// Calls a method on the variable with parameters. + /// - Parameters: + /// - methodName: The name of the method to call. + /// - params: A ``ParameterExpBuilder`` that provides the parameters for the method call. + /// - Returns: A ``FunctionCallExp`` that represents the method call. + public func call(_ methodName: String, @ParameterExpBuilderResult _ params: () -> [ParameterExp]) + -> CodeBlock + { + FunctionCallExp(baseName: name, methodName: methodName, parameters: params()) + } + + public var syntax: SyntaxProtocol { + ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier(name))) + } + + public var patternSyntax: PatternSyntax { + PatternSyntax(IdentifierPatternSyntax(identifier: .identifier(name))) + } +} diff --git a/Sources/SyntaxKit/VariableKind.swift b/Sources/SyntaxKit/Variables/VariableKind.swift similarity index 100% rename from Sources/SyntaxKit/VariableKind.swift rename to Sources/SyntaxKit/Variables/VariableKind.swift diff --git a/Sources/SyntaxKit/parser/String.swift b/Sources/SyntaxKit/parser/String.swift index ee2b142..709fd92 100644 --- a/Sources/SyntaxKit/parser/String.swift +++ b/Sources/SyntaxKit/parser/String.swift @@ -39,7 +39,11 @@ extension String { ] for (unescaped, escaped) in specialCharacters { string = string.replacingOccurrences( - of: unescaped, with: escaped, options: .literal, range: nil) + of: unescaped, + with: escaped, + options: .literal, + range: nil + ) } return string } diff --git a/Sources/SyntaxKit/parser/TokenVisitor.swift b/Sources/SyntaxKit/parser/TokenVisitor.swift index 09a8e3d..4487dcb 100644 --- a/Sources/SyntaxKit/parser/TokenVisitor.swift +++ b/Sources/SyntaxKit/parser/TokenVisitor.swift @@ -63,15 +63,16 @@ internal final class TokenVisitor: SyntaxRewriter { let graphemeStartColumn: Int if let prefix = String( - locationConverter.sourceLines[start.line - 1].utf8.prefix(start.column - 1)) - { + locationConverter.sourceLines[start.line - 1].utf8.prefix(start.column - 1) + ) { graphemeStartColumn = prefix.utf16.count + 1 } else { graphemeStartColumn = start.column } let graphemeEndColumn: Int - if let prefix = String(locationConverter.sourceLines[end.line - 1].utf8.prefix(end.column - 1)) - { + if let prefix = String( + locationConverter.sourceLines[end.line - 1].utf8.prefix(end.column - 1) + ) { graphemeEndColumn = prefix.utf16.count + 1 } else { graphemeEndColumn = end.column @@ -117,7 +118,8 @@ internal final class TokenVisitor: SyntaxRewriter { } guard allChildren.contains(where: { child in child.keyPathInParent == keyPath }) else { treeNode.structure.append( - StructureProperty(name: name, value: StructureValue(text: "nil"))) + StructureProperty(name: name, value: StructureValue(text: "nil")) + ) continue } @@ -166,7 +168,10 @@ internal final class TokenVisitor: SyntaxRewriter { StructureProperty(name: "Element", value: StructureValue(text: "\(syntax)"))) treeNode.structure.append( StructureProperty( - name: "Count", value: StructureValue(text: "\(node.children(viewMode: .all).count)"))) + name: "Count", + value: StructureValue(text: "\(node.children(viewMode: .all).count)") + ) + ) case .choices: break } diff --git a/Sources/SyntaxKit/parser/TreeNode.swift b/Sources/SyntaxKit/parser/TreeNode.swift index a649c78..29ec74c 100644 --- a/Sources/SyntaxKit/parser/TreeNode.swift +++ b/Sources/SyntaxKit/parser/TreeNode.swift @@ -35,7 +35,11 @@ internal final class TreeNode: Codable { internal var text: String internal var range = SourceRange( - startRow: 0, startColumn: 0, endRow: 0, endColumn: 0) + startRow: 0, + startColumn: 0, + endRow: 0, + endColumn: 0 + ) internal var structure = [StructureProperty]() internal var type: SyntaxType internal var token: Token? diff --git a/Tests/SyntaxKitTests/Integration/ConcurrencyExampleTests.swift b/Tests/SyntaxKitTests/Integration/ConcurrencyExampleTests.swift index 9aac57b..d8352c1 100644 --- a/Tests/SyntaxKitTests/Integration/ConcurrencyExampleTests.swift +++ b/Tests/SyntaxKitTests/Integration/ConcurrencyExampleTests.swift @@ -27,7 +27,8 @@ import Testing // VendingMachine class Class("VendingMachine") { Variable( - .var, name: "inventory", + .var, + name: "inventory", equals: DictionaryExpr([ ( Literal.string("Candy Bar"), @@ -120,9 +121,11 @@ import Testing } class VendingMachine { - var inventory = ["Candy Bar": Item(price: 12, count: 7), - "Chips": Item(price: 10, count: 4), - "Pretzels": Item(price: 7, count: 11)] + var inventory = [ + "Candy Bar": Item(price: 12, count: 7), + "Chips": Item(price: 10, count: 4), + "Pretzels": Item(price: 7, count: 11) + ] var coinsDeposited = 0 func vend(itemNamed name: String) throws { diff --git a/Tests/SyntaxKitTests/Integration/ConditionalsExampleTests.swift b/Tests/SyntaxKitTests/Integration/ConditionalsExampleTests.swift index 821c215..f5170c6 100644 --- a/Tests/SyntaxKitTests/Integration/ConditionalsExampleTests.swift +++ b/Tests/SyntaxKitTests/Integration/ConditionalsExampleTests.swift @@ -93,11 +93,11 @@ import Testing Call("print") { ParameterExp( name: "", - value: - "\"The string \"\\(possibleNumber)\" could not be converted to an integer\"" + value: "\"The string \"\\(possibleNumber)\" could not be converted to an integer\"" ) } - }) + } + ) // Multiple optional bindings Variable(.let, name: "possibleName", type: "String?", equals: Literal.string("John")) @@ -118,28 +118,34 @@ import Testing } // MARK: - Guard Statements - Function("greet", { Parameter(name: "person", type: "[String: String]") }) { - Guard { - Let("name", "person[\"name\"]") - } else: { - Call("print") { - ParameterExp(name: "", value: "\"No name provided\"") + Function( + "greet", + { + Parameter(name: "person", type: "[String: String]") + }, + { + Guard { + Let("name", "person[\"name\"]") + } else: { + Call("print") { + ParameterExp(name: "", value: "\"No name provided\"") + } } - } - Guard { - Let("age", "person[\"age\"]") - Let("ageInt", "Int(age)") - } else: { - Call("print") { - ParameterExp(name: "", value: "\"Invalid age provided\"") + Guard { + Let("age", "person[\"age\"]") + Let("ageInt", "Int(age)") + } else: { + Call("print") { + ParameterExp(name: "", value: "\"Invalid age provided\"") + } } - } - Call("print") { - ParameterExp(name: "", value: "\"Hello \\(name), you are \\(ageInt) years old\"") + Call("print") { + ParameterExp(name: "", value: "\"Hello \\(name), you are \\(ageInt) years old\"") + } } - } + ) .comment { Line("MARK: - Guard Statements") } diff --git a/Tests/SyntaxKitTests/Unit/AttributeTests.swift b/Tests/SyntaxKitTests/Unit/Attributes/AttributeTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/AttributeTests.swift rename to Tests/SyntaxKitTests/Unit/Attributes/AttributeTests.swift diff --git a/Tests/SyntaxKitTests/Unit/TupleAssignmentAsyncTests.swift b/Tests/SyntaxKitTests/Unit/Collections/TupleAssignmentAsyncTests.swift similarity index 94% rename from Tests/SyntaxKitTests/Unit/TupleAssignmentAsyncTests.swift rename to Tests/SyntaxKitTests/Unit/Collections/TupleAssignmentAsyncTests.swift index 9578aba..4227fd1 100644 --- a/Tests/SyntaxKitTests/Unit/TupleAssignmentAsyncTests.swift +++ b/Tests/SyntaxKitTests/Unit/Collections/TupleAssignmentAsyncTests.swift @@ -34,7 +34,7 @@ import Testing ).async() let generated = tupleAssignment.generateCode() - let expected = "let (result, count) = await (processData(input: \"test\"), 42)" + let expected = "let (result, count) = await (processData(input: \"test\"), " + "42)" #expect(generated.normalize() == expected.normalize()) } @@ -73,7 +73,7 @@ import Testing let generated = tupleAssignment.generateCode() let expected = - "let (data, posts) = try (await fetchUserData(id: 1), await fetchUserPosts(id: 1))" + "let (data, posts) = try (await fetchUserData(id: 1), " + "await fetchUserPosts(id: 1))" #expect(generated.normalize() == expected.normalize()) } @@ -115,7 +115,8 @@ import Testing let generated = tupleAssignment.generateCode() let expected = - "let (user, profile, settings) = try await (await fetchUser(id: 123), await fetchProfile(userId: 123), await fetchSettings(userId: 123))" + "let (user, profile, settings) = try await (await fetchUser(id: 123), " + + "await fetchProfile(userId: 123), await fetchSettings(userId: 123))" #expect(generated.normalize() == expected.normalize()) } diff --git a/Tests/SyntaxKitTests/Unit/TupleAssignmentBasicTests.swift b/Tests/SyntaxKitTests/Unit/Collections/TupleAssignmentBasicTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/TupleAssignmentBasicTests.swift rename to Tests/SyntaxKitTests/Unit/Collections/TupleAssignmentBasicTests.swift diff --git a/Tests/SyntaxKitTests/Unit/TupleAssignmentEdgeCaseTests.swift b/Tests/SyntaxKitTests/Unit/Collections/TupleAssignmentEdgeCaseTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/TupleAssignmentEdgeCaseTests.swift rename to Tests/SyntaxKitTests/Unit/Collections/TupleAssignmentEdgeCaseTests.swift diff --git a/Tests/SyntaxKitTests/Unit/TupleAssignmentIntegrationTests.swift b/Tests/SyntaxKitTests/Unit/Collections/TupleAssignmentIntegrationTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/TupleAssignmentIntegrationTests.swift rename to Tests/SyntaxKitTests/Unit/Collections/TupleAssignmentIntegrationTests.swift diff --git a/Tests/SyntaxKitTests/Unit/ConditionalsTests.swift b/Tests/SyntaxKitTests/Unit/ControlFlow/ConditionalsTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/ConditionalsTests.swift rename to Tests/SyntaxKitTests/Unit/ControlFlow/ConditionalsTests.swift diff --git a/Tests/SyntaxKitTests/Unit/ForLoopTests.swift b/Tests/SyntaxKitTests/Unit/ControlFlow/ForLoopTests.swift similarity index 88% rename from Tests/SyntaxKitTests/Unit/ForLoopTests.swift rename to Tests/SyntaxKitTests/Unit/ControlFlow/ForLoopTests.swift index f5fd0b2..3b922cf 100644 --- a/Tests/SyntaxKitTests/Unit/ForLoopTests.swift +++ b/Tests/SyntaxKitTests/Unit/ControlFlow/ForLoopTests.swift @@ -3,9 +3,9 @@ import Testing @testable import SyntaxKit @Suite -final class ForLoopTests { +internal final class ForLoopTests { @Test - func testSimpleForInLoop() { + internal func testSimpleForInLoop() { let forLoop = For( VariableExp("item"), in: VariableExp("items"), @@ -22,7 +22,7 @@ final class ForLoopTests { } @Test - func testForInWithWhereClause() { + internal func testForInWithWhereClause() { let forLoop = For( VariableExp("number"), in: VariableExp("numbers"), diff --git a/Tests/SyntaxKitTests/Unit/PatternConvertibleTests.swift b/Tests/SyntaxKitTests/Unit/Core/PatternConvertibleTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/PatternConvertibleTests.swift rename to Tests/SyntaxKitTests/Unit/Core/PatternConvertibleTests.swift diff --git a/Tests/SyntaxKitTests/Unit/ClassTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/ClassTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/ClassTests.swift rename to Tests/SyntaxKitTests/Unit/Declarations/ClassTests.swift diff --git a/Tests/SyntaxKitTests/Unit/ExtensionTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/ExtensionTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/ExtensionTests.swift rename to Tests/SyntaxKitTests/Unit/Declarations/ExtensionTests.swift diff --git a/Tests/SyntaxKitTests/Unit/ProtocolTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/ProtocolTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/ProtocolTests.swift rename to Tests/SyntaxKitTests/Unit/Declarations/ProtocolTests.swift diff --git a/Tests/SyntaxKitTests/Unit/StructTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/StructTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/StructTests.swift rename to Tests/SyntaxKitTests/Unit/Declarations/StructTests.swift diff --git a/Tests/SyntaxKitTests/Unit/TypeAliasTests.swift b/Tests/SyntaxKitTests/Unit/Declarations/TypeAliasTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/TypeAliasTests.swift rename to Tests/SyntaxKitTests/Unit/Declarations/TypeAliasTests.swift diff --git a/Tests/SyntaxKitTests/Unit/EdgeCaseTests.swift b/Tests/SyntaxKitTests/Unit/EdgeCases/EdgeCaseTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/EdgeCaseTests.swift rename to Tests/SyntaxKitTests/Unit/EdgeCases/EdgeCaseTests.swift diff --git a/Tests/SyntaxKitTests/Unit/EdgeCaseTestsExpressions.swift b/Tests/SyntaxKitTests/Unit/EdgeCases/EdgeCaseTestsExpressions.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/EdgeCaseTestsExpressions.swift rename to Tests/SyntaxKitTests/Unit/EdgeCases/EdgeCaseTestsExpressions.swift diff --git a/Tests/SyntaxKitTests/Unit/EdgeCaseTestsTypes.swift b/Tests/SyntaxKitTests/Unit/EdgeCases/EdgeCaseTestsTypes.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/EdgeCaseTestsTypes.swift rename to Tests/SyntaxKitTests/Unit/EdgeCases/EdgeCaseTestsTypes.swift diff --git a/Tests/SyntaxKitTests/Unit/CatchBasicTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/CatchBasicTests.swift similarity index 97% rename from Tests/SyntaxKitTests/Unit/CatchBasicTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/CatchBasicTests.swift index dcaad97..3cb1b6f 100644 --- a/Tests/SyntaxKitTests/Unit/CatchBasicTests.swift +++ b/Tests/SyntaxKitTests/Unit/ErrorHandling/CatchBasicTests.swift @@ -126,7 +126,8 @@ import Testing let generated = doCatch.generateCode() let expected = - "do { try someFunction(param: \"test\") } catch { logError(error: error) print(\"Error: \\(error)\") }" + "do { try someFunction(param: \"test\") } catch { " + "logError(error: error) " + + "print(\"Error: \\(error)\") }" #expect(generated.normalize() == expected.normalize()) } diff --git a/Tests/SyntaxKitTests/Unit/CatchComplexTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/CatchComplexTests.swift similarity index 86% rename from Tests/SyntaxKitTests/Unit/CatchComplexTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/CatchComplexTests.swift index 5fbb362..7356db3 100644 --- a/Tests/SyntaxKitTests/Unit/CatchComplexTests.swift +++ b/Tests/SyntaxKitTests/Unit/ErrorHandling/CatchComplexTests.swift @@ -26,7 +26,8 @@ import Testing let generated = doCatch.generateCode() let expected = - "do { try someFunction(param: \"test\") } catch .requestFailed(let statusCode, let message) { " + "do { try someFunction(param: \"test\") } " + + "catch .requestFailed(let statusCode, let message) { " + "logAPIError(statusCode: statusCode, message: message) }" #expect(generated.normalize() == expected.normalize()) @@ -49,7 +50,8 @@ import Testing let generated = doCatch.generateCode() let expected = - "do { try someFunction(param: \"test\") } catch .connectionFailed { retryConnection(maxAttempts: 3) }" + "do { try someFunction(param: \"test\") } " + + "catch .connectionFailed { retryConnection(maxAttempts: 3) }" #expect(generated.normalize() == expected.normalize()) } @@ -76,7 +78,11 @@ import Testing let generated = doCatch.generateCode() let expected = """ - do { try someFunction(param: "test") } catch .invalidEmail { logValidationError(field: "email") let errorMessage = "Invalid email format" showError(message: errorMessage) } + do { try someFunction(param: "test") } catch .invalidEmail { + logValidationError(field: "email") + let errorMessage = "Invalid email format" + showError(message: errorMessage) + } """ #expect(generated.normalize() == expected.normalize()) @@ -102,7 +108,11 @@ import Testing let generated = doCatch.generateCode() let expected = """ - do { try someFunction(param: "test") } catch .connectionFailed { let retryCount = 0 attemptConnection(attempt: retryCount) incrementRetryCount(current: retryCount) } + do { try someFunction(param: "test") } catch .connectionFailed { + let retryCount = 0 + attemptConnection(attempt: retryCount) + incrementRetryCount(current: retryCount) + } """ #expect(generated.normalize() == expected.normalize()) diff --git a/Tests/SyntaxKitTests/Unit/CatchEdgeCaseTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/CatchEdgeCaseTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/CatchEdgeCaseTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/CatchEdgeCaseTests.swift diff --git a/Tests/SyntaxKitTests/Unit/CatchIntegrationTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/CatchIntegrationTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/CatchIntegrationTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/CatchIntegrationTests.swift diff --git a/Tests/SyntaxKitTests/Unit/DoBasicTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/DoBasicTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/DoBasicTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/DoBasicTests.swift diff --git a/Tests/SyntaxKitTests/Unit/DoComplexTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/DoComplexTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/DoComplexTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/DoComplexTests.swift diff --git a/Tests/SyntaxKitTests/Unit/DoEdgeCaseTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/DoEdgeCaseTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/DoEdgeCaseTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/DoEdgeCaseTests.swift diff --git a/Tests/SyntaxKitTests/Unit/DoIntegrationTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/DoIntegrationTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/DoIntegrationTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/DoIntegrationTests.swift diff --git a/Tests/SyntaxKitTests/Unit/ErrorHandlingTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/ErrorHandlingTests.swift similarity index 90% rename from Tests/SyntaxKitTests/Unit/ErrorHandlingTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/ErrorHandlingTests.swift index e68a119..ecf8694 100644 --- a/Tests/SyntaxKitTests/Unit/ErrorHandlingTests.swift +++ b/Tests/SyntaxKitTests/Unit/ErrorHandling/ErrorHandlingTests.swift @@ -3,6 +3,7 @@ import Testing @testable import SyntaxKit @Suite internal struct ErrorHandlingTests { + // swiftlint:disable function_body_length @Test("Error handling DSL generates expected Swift code") internal func testErrorHandlingExample() throws { let errorHandlingExample = Group { @@ -52,16 +53,16 @@ import Testing var vendingMachine = VendingMachine() vendingMachine.coinsDeposited = 8 do { - try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine) - print("Success! Yum.") + try buyFavoriteSnack(person: "Alice", vendingMachine: vendingMachine) + print("Success! Yum.") } catch .invalidSelection { - print("Invalid Selection.") + print("Invalid Selection.") } catch .outOfStock { - print("Out of Stock.") + print("Out of Stock.") } catch .insufficientFunds(let coinsNeeded) { - print("Insufficient funds. Please insert an additional \\(coinsNeeded) coins.") + print("Insufficient funds. Please insert an additional \\(coinsNeeded) coins.") } catch { - print("Unexpected error: \\(error).") + print("Unexpected error: \\(error).") } """ @@ -72,6 +73,7 @@ import Testing print("Generated code:") print(generated) } + // swiftlint:enable function_body_length @Test("Function with throws clause and unlabeled parameter generates correct syntax") internal func testFunctionWithThrowsClauseAndUnlabeledParameter() throws { diff --git a/Tests/SyntaxKitTests/Unit/ThrowBasicTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/ThrowBasicTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/ThrowBasicTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/ThrowBasicTests.swift diff --git a/Tests/SyntaxKitTests/Unit/ThrowComplexTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/ThrowComplexTests.swift similarity index 96% rename from Tests/SyntaxKitTests/Unit/ThrowComplexTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/ThrowComplexTests.swift index eb8700c..db5c14f 100644 --- a/Tests/SyntaxKitTests/Unit/ThrowComplexTests.swift +++ b/Tests/SyntaxKitTests/Unit/ErrorHandling/ThrowComplexTests.swift @@ -101,7 +101,7 @@ import Testing Parameter(name: "id", type: "Int") } _: { Guard { - VariableExp("id") > Literal.integer(0) + Infix(.greaterThan, lhs: VariableExp("id"), rhs: Literal.integer(0)) } else: { Throw(EnumCase("invalidId")) } @@ -111,7 +111,7 @@ import Testing } }.async() Guard { - VariableExp("user") != Literal.nil + Infix(.notEqual, lhs: VariableExp("user"), rhs: Literal.nil) } else: { Throw(EnumCase("userNotFound")) } diff --git a/Tests/SyntaxKitTests/Unit/ThrowEdgeCaseTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/ThrowEdgeCaseTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/ThrowEdgeCaseTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/ThrowEdgeCaseTests.swift diff --git a/Tests/SyntaxKitTests/Unit/ThrowFunctionTests.swift b/Tests/SyntaxKitTests/Unit/ErrorHandling/ThrowFunctionTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/ThrowFunctionTests.swift rename to Tests/SyntaxKitTests/Unit/ErrorHandling/ThrowFunctionTests.swift diff --git a/Tests/SyntaxKitTests/Unit/CallTests.swift b/Tests/SyntaxKitTests/Unit/Expressions/CallTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/CallTests.swift rename to Tests/SyntaxKitTests/Unit/Expressions/CallTests.swift diff --git a/Tests/SyntaxKitTests/Unit/LiteralTests.swift b/Tests/SyntaxKitTests/Unit/Expressions/LiteralTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/LiteralTests.swift rename to Tests/SyntaxKitTests/Unit/Expressions/LiteralTests.swift diff --git a/Tests/SyntaxKitTests/Unit/LiteralValueTests.swift b/Tests/SyntaxKitTests/Unit/Expressions/LiteralValueTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/LiteralValueTests.swift rename to Tests/SyntaxKitTests/Unit/Expressions/LiteralValueTests.swift diff --git a/Tests/SyntaxKitTests/Unit/FunctionTests.swift b/Tests/SyntaxKitTests/Unit/Functions/FunctionTests.swift similarity index 86% rename from Tests/SyntaxKitTests/Unit/FunctionTests.swift rename to Tests/SyntaxKitTests/Unit/Functions/FunctionTests.swift index 5ae2c2f..d6e25bb 100644 --- a/Tests/SyntaxKitTests/Unit/FunctionTests.swift +++ b/Tests/SyntaxKitTests/Unit/Functions/FunctionTests.swift @@ -29,17 +29,19 @@ internal struct FunctionTests { @Test internal func testStaticFunction() throws { let function = Function( - "createInstance", returns: "MyType", + "createInstance", + returns: "MyType", { Parameter(name: "value", type: "String") - } - ) { - Return { - Init("MyType") { - ParameterExp(name: "value", value: Literal.ref("value")) + }, + { + Return { + Init("MyType") { + ParameterExp(name: "value", value: Literal.ref("value")) + } } } - }.static() + ).static() let expected = """ static func createInstance(value: String) -> MyType { @@ -60,10 +62,11 @@ internal struct FunctionTests { "updateValue", { Parameter(name: "newValue", type: "String") + }, + { + Assignment("value", Literal.ref("newValue")) } - ) { - Assignment("value", Literal.ref("newValue")) - }.mutating() + ).mutating() let expected = """ mutating func updateValue(newValue: String) { diff --git a/Tests/SyntaxKitTests/Unit/FrameworkCompatibilityTests.swift b/Tests/SyntaxKitTests/Unit/Integration/FrameworkCompatibilityTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/FrameworkCompatibilityTests.swift rename to Tests/SyntaxKitTests/Unit/Integration/FrameworkCompatibilityTests.swift diff --git a/Tests/SyntaxKitTests/Unit/OptionsMacroIntegrationTests.swift b/Tests/SyntaxKitTests/Unit/Integration/OptionsMacroIntegrationTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/OptionsMacroIntegrationTests.swift rename to Tests/SyntaxKitTests/Unit/Integration/OptionsMacroIntegrationTests.swift diff --git a/Tests/SyntaxKitTests/Unit/OptionsMacroIntegrationTestsAPI.swift b/Tests/SyntaxKitTests/Unit/Integration/OptionsMacroIntegrationTestsAPI.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/OptionsMacroIntegrationTestsAPI.swift rename to Tests/SyntaxKitTests/Unit/Integration/OptionsMacroIntegrationTestsAPI.swift diff --git a/Tests/SyntaxKitTests/Unit/MainApplicationTests.swift b/Tests/SyntaxKitTests/Unit/MainApplicationTests.swift deleted file mode 100644 index 133d548..0000000 --- a/Tests/SyntaxKitTests/Unit/MainApplicationTests.swift +++ /dev/null @@ -1,198 +0,0 @@ -import Foundation -import Testing - -@testable import SyntaxKit - -internal struct MainApplicationTests { - // MARK: - Main Application Error Handling Tests - - @Test("Main application handles valid input") - internal func testMainApplicationValidInput() throws { - // This test simulates the main application behavior - // We can't easily test the main function directly, but we can test its components - - let code = "let x = 42" - let response = try SyntaxParser.parse(code: code, options: ["fold"]) - - // Test JSON serialization (part of main application logic) - let jsonData = try JSONSerialization.data(withJSONObject: ["syntax": response.syntaxJSON]) - let jsonString = String(data: jsonData, encoding: .utf8) - - #expect(jsonString != nil) - #expect(jsonString!.contains("syntax")) - #expect(jsonString!.contains("let")) - } - - @Test("Main application handles empty input") - internal func testMainApplicationEmptyInput() throws { - let code = "" - let response = try SyntaxParser.parse(code: code, options: []) - - // Test JSON serialization with empty result - let jsonData = try JSONSerialization.data(withJSONObject: ["syntax": response.syntaxJSON]) - let jsonString = String(data: jsonData, encoding: .utf8) - - #expect(jsonString != nil) - #expect(jsonString!.contains("syntax")) - } - - @Test("Main application handles parsing errors") - internal func testMainApplicationHandlesParsingErrors() throws { - let invalidCode = "struct {" - - // The parser doesn't throw errors for invalid syntax, it returns a result - let response = try SyntaxParser.parse(code: invalidCode, options: []) - - // Test error JSON serialization (part of main application logic) - let jsonData = try JSONSerialization.data(withJSONObject: ["error": "Invalid syntax"]) - let jsonString = String(data: jsonData, encoding: .utf8) - - #expect(jsonString != nil) - #expect(jsonString!.contains("error")) - #expect(jsonString!.contains("Invalid syntax")) - } - - @Test("Main application handles JSON serialization errors") - internal func testMainApplicationHandlesJSONSerializationErrors() throws { - // Test with a response that might cause JSON serialization issues - let code = "let x = 42" - let response = try SyntaxParser.parse(code: code, options: []) - - // This should work fine, but we're testing the JSON serialization path - let jsonData = try JSONSerialization.data(withJSONObject: ["syntax": response.syntaxJSON]) - let jsonString = String(data: jsonData, encoding: .utf8) - - #expect(jsonString != nil) - } - - // MARK: - File I/O Simulation Tests - - @Test("Main application handles large input") - internal func testMainApplicationHandlesLargeInput() throws { - // Generate a large Swift file to test performance - var largeCode = "" - for index in 1...50 { - largeCode += """ - struct Struct\(index) { - let property\(index): String - func method\(index)() -> String { - return "value\(index)" - } - } - - """ - } - - let response = try SyntaxParser.parse(code: largeCode, options: ["fold"]) - let jsonData = try JSONSerialization.data(withJSONObject: ["syntax": response.syntaxJSON]) - let jsonString = String(data: jsonData, encoding: .utf8) - - #expect(jsonString != nil) - #expect(jsonString!.contains("Struct1")) - #expect(jsonString!.contains("Struct50")) - } - - @Test("Main application handles unicode input") - internal func testMainApplicationHandlesUnicodeInput() throws { - let unicodeCode = """ - let emoji = "🚀" - let unicode = "café" - let chinese = "你好" - """ - - let response = try SyntaxParser.parse(code: unicodeCode, options: []) - let jsonData = try JSONSerialization.data(withJSONObject: ["syntax": response.syntaxJSON]) - let jsonString = String(data: jsonData, encoding: .utf8) - - #expect(jsonString != nil) - #expect(jsonString!.contains("emoji")) - #expect(jsonString!.contains("unicode")) - #expect(jsonString!.contains("chinese")) - } - - // MARK: - Error Response Format Tests - - @Test("Main application error response format") - internal func testMainApplicationErrorResponseFormat() throws { - // Test the error response format that the main application would generate - let testError = NSError( - domain: "TestDomain", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "Test error message"] - ) - - let errorResponse = ["error": testError.localizedDescription] - let jsonData = try JSONSerialization.data(withJSONObject: errorResponse) - let jsonString = String(data: jsonData, encoding: .utf8) - - #expect(jsonString != nil) - #expect(jsonString!.contains("error")) - #expect(jsonString!.contains("Test error message")) - } - - @Test("Main application handles encoding errors") - internal func testMainApplicationHandlesEncodingErrors() throws { - let code = "let x = 42" - let response = try SyntaxParser.parse(code: code, options: []) - - // Test UTF-8 encoding (part of main application logic) - let jsonData = try JSONSerialization.data(withJSONObject: ["syntax": response.syntaxJSON]) - let jsonString = String(data: jsonData, encoding: .utf8) - - #expect(jsonString != nil) - #expect(jsonString!.contains("syntax")) - } - - // MARK: - Integration Tests - - @Test("Main application integration with complex Swift code") - internal func testMainApplicationIntegrationWithComplexSwiftCode() throws { - let code = """ - @objc class MyClass: NSObject { - @Published var property: String = "default" - - func method(@escaping completion: @escaping (String) -> Void) { - completion("result") - } - - enum NestedEnum: Int { - case first = 1 - case second = 2 - } - } - """ - - let response = try SyntaxParser.parse(code: code, options: ["fold"]) - - // Test JSON serialization (part of main application logic) - let jsonData = try JSONSerialization.data(withJSONObject: ["syntax": response.syntaxJSON]) - let jsonString = String(data: jsonData, encoding: .utf8) - - #expect(jsonString != nil) - #expect(jsonString!.contains("syntax")) - #expect(jsonString!.contains("class")) - #expect(jsonString!.contains("MyClass")) - } - - @Test("Main application handles different parser options") - internal func testMainApplicationHandlesDifferentParserOptions() throws { - let code = "let x = 42" - - let response1 = try SyntaxParser.parse(code: code, options: []) - let response2 = try SyntaxParser.parse(code: code, options: ["fold"]) - - // Test JSON serialization for both responses - let jsonData1 = try JSONSerialization.data(withJSONObject: ["syntax": response1.syntaxJSON]) - let jsonString1 = String(data: jsonData1, encoding: .utf8) - - let jsonData2 = try JSONSerialization.data(withJSONObject: ["syntax": response2.syntaxJSON]) - let jsonString2 = String(data: jsonData2, encoding: .utf8) - - #expect(jsonString1 != nil) - #expect(jsonString2 != nil) - #expect(jsonString1!.contains("syntax")) - #expect(jsonString2!.contains("syntax")) - #expect(jsonString1!.contains("let")) - #expect(jsonString2!.contains("let")) - } -} diff --git a/Tests/SyntaxKitTests/Unit/AssertionMigrationTests.swift b/Tests/SyntaxKitTests/Unit/Migration/AssertionMigrationTests.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/AssertionMigrationTests.swift rename to Tests/SyntaxKitTests/Unit/Migration/AssertionMigrationTests.swift diff --git a/Tests/SyntaxKitTests/Unit/CodeStyleMigrationTests.swift b/Tests/SyntaxKitTests/Unit/Migration/CodeStyleMigrationTests.swift similarity index 95% rename from Tests/SyntaxKitTests/Unit/CodeStyleMigrationTests.swift rename to Tests/SyntaxKitTests/Unit/Migration/CodeStyleMigrationTests.swift index 9591a6e..c92a763 100644 --- a/Tests/SyntaxKitTests/Unit/CodeStyleMigrationTests.swift +++ b/Tests/SyntaxKitTests/Unit/Migration/CodeStyleMigrationTests.swift @@ -48,7 +48,7 @@ internal struct CodeStyleMigrationTests { // MARK: - Multiline String Formatting Tests - @Test func testMultilineStringFormatting() { + @Test internal func testMultilineStringFormatting() { let expected = """ struct TestStruct { let value: String @@ -68,7 +68,7 @@ internal struct CodeStyleMigrationTests { #expect(normalized == expectedNormalized) } - @Test func testMigrationPreservesCodeGeneration() { + @Test internal func testMigrationPreservesCodeGeneration() { // Ensure that the style changes don't break core functionality let group = Group { Return { diff --git a/Tests/SyntaxKitTests/Unit/MigrationTests.swift b/Tests/SyntaxKitTests/Unit/Migration/MigrationTests.swift similarity index 98% rename from Tests/SyntaxKitTests/Unit/MigrationTests.swift rename to Tests/SyntaxKitTests/Unit/Migration/MigrationTests.swift index ed00bc1..3564d8f 100644 --- a/Tests/SyntaxKitTests/Unit/MigrationTests.swift +++ b/Tests/SyntaxKitTests/Unit/Migration/MigrationTests.swift @@ -82,7 +82,10 @@ internal struct MigrationTests { // Test that .regularExpression works instead of String.CompareOptions.regularExpression let testString = "public func test() { }" let result = testString.replacingOccurrences( - of: "public\\s+", with: "", options: .regularExpression) + of: "public\\s+", + with: "", + options: .regularExpression + ) let expected = "func test() { }" #expect(result == expected) } diff --git a/Tests/SyntaxKitTests/Unit/String+Normalize.swift b/Tests/SyntaxKitTests/Unit/Utilities/String+Normalize.swift similarity index 100% rename from Tests/SyntaxKitTests/Unit/String+Normalize.swift rename to Tests/SyntaxKitTests/Unit/Utilities/String+Normalize.swift diff --git a/Tests/SyntaxKitTests/Unit/VariableStaticTests.swift b/Tests/SyntaxKitTests/Unit/Variables/VariableStaticTests.swift similarity index 99% rename from Tests/SyntaxKitTests/Unit/VariableStaticTests.swift rename to Tests/SyntaxKitTests/Unit/Variables/VariableStaticTests.swift index 3ee8dc0..f5fa928 100644 --- a/Tests/SyntaxKitTests/Unit/VariableStaticTests.swift +++ b/Tests/SyntaxKitTests/Unit/Variables/VariableStaticTests.swift @@ -156,7 +156,8 @@ internal struct VariableStaticTests { @Test internal func testMultipleStaticCalls() { let variable = Variable(.let, name: "test", type: "String", equals: Literal.ref("value")) .withExplicitType() - .static().static() + .static() + .static() let generated = variable.generateCode().normalize() // Should still only have one "static" keyword